mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-06 13:40:25 +00:00
merge main into rh-test
This commit is contained in:
70
src/views/DesktopDialogView.vue
Normal file
70
src/views/DesktopDialogView.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
|
||||
<h1 class="font-inter font-semibold text-xl m-0 italic">
|
||||
{{ t(`desktopDialogs.${id}.title`, title) }}
|
||||
</h1>
|
||||
<p class="whitespace-pre-wrap">
|
||||
{{ t(`desktopDialogs.${id}.message`, message) }}
|
||||
</p>
|
||||
<div class="flex w-full gap-2">
|
||||
<Button
|
||||
v-for="button in buttons"
|
||||
:key="button.label"
|
||||
class="rounded-lg first:mr-auto"
|
||||
:label="
|
||||
t(
|
||||
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
|
||||
button.label
|
||||
)
|
||||
"
|
||||
:severity="button.severity ?? 'secondary'"
|
||||
@click="handleButtonClick(button)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { type DialogAction, getDialog } from '@/constants/desktopDialogs'
|
||||
import { t } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const route = useRoute()
|
||||
const { id, title, message, buttons } = getDialog(route.params.dialogId)
|
||||
|
||||
const handleButtonClick = async (button: DialogAction) => {
|
||||
await electronAPI().Dialog.clickButton(button.returnValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.p-button-secondary {
|
||||
@apply text-white border-none bg-neutral-600;
|
||||
}
|
||||
|
||||
.p-button-secondary:hover {
|
||||
@apply bg-neutral-550;
|
||||
}
|
||||
|
||||
.p-button-secondary:active {
|
||||
@apply bg-neutral-500;
|
||||
}
|
||||
|
||||
.p-button-danger {
|
||||
@apply bg-coral-red-600;
|
||||
}
|
||||
|
||||
.p-button-danger:hover {
|
||||
@apply bg-coral-red-500;
|
||||
}
|
||||
|
||||
.p-button-danger:active {
|
||||
@apply bg-coral-red-400;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
||||
<StartupDisplay :title="$t('desktopStart.initialising')" />
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import StartupDisplay from '@/components/common/StartupDisplay.vue'
|
||||
|
||||
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
||||
</script>
|
||||
|
||||
@@ -61,6 +61,8 @@ onUnmounted(() => electron.Validation.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.download-bg::before {
|
||||
@apply m-0 absolute text-muted;
|
||||
font-family: 'primeicons';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseViewTemplate>
|
||||
<div
|
||||
class="max-w-screen-sm flex flex-col gap-8 p-8 bg-[url('/assets/images/Git-Logo-White.svg')] bg-no-repeat bg-right-top bg-origin-padding"
|
||||
class="max-w-(--breakpoint-sm) flex flex-col gap-8 p-8 bg-[url('/assets/images/Git-Logo-White.svg')] bg-no-repeat bg-top-right bg-origin-padding"
|
||||
>
|
||||
<!-- Header -->
|
||||
<h1 class="mt-24 text-4xl font-bold text-red-500">
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { runWhenGlobalIdle } from '@/base/common/async'
|
||||
import MenuHamburger from '@/components/MenuHamburger.vue'
|
||||
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
|
||||
@@ -42,11 +43,13 @@ import TopMenubar from '@/components/topbar/TopMenubar.vue'
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
import { useCoreCommands } from '@/composables/useCoreCommands'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useFrontendVersionMismatchWarning } from '@/composables/useFrontendVersionMismatchWarning'
|
||||
import { useProgressFavicon } from '@/composables/useProgressFavicon'
|
||||
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
|
||||
import { i18n } from '@/i18n'
|
||||
import { StatusWsMessageStatus } from '@/schemas/apiSchema'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning'
|
||||
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
|
||||
import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
||||
@@ -61,8 +64,6 @@ import {
|
||||
useQueueStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
@@ -253,33 +254,30 @@ void nextTick(() => {
|
||||
})
|
||||
|
||||
const onGraphReady = () => {
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
// Setting values now available after comfyApp.setup.
|
||||
// Load keybindings.
|
||||
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
|
||||
runWhenGlobalIdle(() => {
|
||||
// Setting values now available after comfyApp.setup.
|
||||
// Load keybindings.
|
||||
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
|
||||
|
||||
// Load server config
|
||||
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
|
||||
SERVER_CONFIG_ITEMS,
|
||||
settingStore.get('Comfy.Server.ServerConfigValues')
|
||||
)
|
||||
// Load server config
|
||||
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
|
||||
SERVER_CONFIG_ITEMS,
|
||||
settingStore.get('Comfy.Server.ServerConfigValues')
|
||||
)
|
||||
|
||||
// Load model folders
|
||||
void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()
|
||||
// Load model folders
|
||||
void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()
|
||||
|
||||
// Non-blocking load of node frequencies
|
||||
void wrapWithErrorHandlingAsync(
|
||||
useNodeFrequencyStore().loadNodeFrequencies
|
||||
)()
|
||||
// Non-blocking load of node frequencies
|
||||
void wrapWithErrorHandlingAsync(
|
||||
useNodeFrequencyStore().loadNodeFrequencies
|
||||
)()
|
||||
|
||||
// Node defs now available after comfyApp.setup.
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.searchNode('')
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
)
|
||||
// Node defs now available after comfyApp.setup.
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.searchNode('')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -343,7 +341,7 @@ const onGraphReady = () => {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.comfyui-body-right {
|
||||
|
||||
423
src/views/InstallView.stories.ts
Normal file
423
src/views/InstallView.stories.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { nextTick, provide } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import InstallView from './InstallView.vue'
|
||||
|
||||
// Create a mock router for stories
|
||||
const createMockRouter = () =>
|
||||
createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
{
|
||||
path: '/server-start',
|
||||
component: { template: '<div>Server Start</div>' }
|
||||
},
|
||||
{
|
||||
path: '/manual-configuration',
|
||||
component: { template: '<div>Manual Configuration</div>' }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const meta: Meta<typeof InstallView> = {
|
||||
title: 'Desktop/Views/InstallView',
|
||||
component: InstallView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
(story) => {
|
||||
// Create router for this story
|
||||
const router = createMockRouter()
|
||||
|
||||
// Mock electron API
|
||||
;(window as any).electronAPI = {
|
||||
getPlatform: () => 'darwin',
|
||||
Config: {
|
||||
getDetectedGpu: () => Promise.resolve('mps')
|
||||
},
|
||||
Events: {
|
||||
trackEvent: (eventName: string, data?: any) => {
|
||||
console.log('Track event:', eventName, data)
|
||||
}
|
||||
},
|
||||
installComfyUI: (options: any) => {
|
||||
console.log('Install ComfyUI with options:', options)
|
||||
},
|
||||
changeTheme: (theme: any) => {
|
||||
console.log('Change theme:', theme)
|
||||
},
|
||||
getSystemPaths: () =>
|
||||
Promise.resolve({
|
||||
defaultInstallPath: '/Users/username/ComfyUI'
|
||||
}),
|
||||
validateInstallPath: () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false
|
||||
}),
|
||||
validateComfyUISource: () =>
|
||||
Promise.resolve({
|
||||
isValid: true
|
||||
}),
|
||||
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
||||
}
|
||||
|
||||
return {
|
||||
setup() {
|
||||
// Provide router for all child components
|
||||
provide('router', router)
|
||||
return {
|
||||
story
|
||||
}
|
||||
},
|
||||
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Default story - start at GPU selection
|
||||
export const GpuSelection: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
// The component will automatically start at step 1
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the install location step
|
||||
export const InstallLocation: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the migration step (currently empty)
|
||||
export const MigrationStep: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons1 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn1) {
|
||||
nextBtn1.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 3
|
||||
const buttons2 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn2) {
|
||||
nextBtn2.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story showing the desktop settings configuration
|
||||
export const DesktopSettings: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2
|
||||
const buttons1 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn1) {
|
||||
nextBtn1.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 3
|
||||
const buttons2 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn2) {
|
||||
nextBtn2.click()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next again to go to step 4
|
||||
const buttons3 = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn3) {
|
||||
nextBtn3.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with Windows platform (no Apple Metal option)
|
||||
export const WindowsPlatform: Story = {
|
||||
render: () => {
|
||||
// Override the platform to Windows
|
||||
;(window as any).electronAPI.getPlatform = () => 'win32'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('nvidia')
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with macOS platform (Apple Metal option)
|
||||
export const MacOSPlatform: Story = {
|
||||
name: 'macOS Platform',
|
||||
render: () => {
|
||||
// Override the platform to macOS
|
||||
;(window as any).electronAPI.getPlatform = () => 'darwin'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('mps')
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with CPU selected
|
||||
export const CpuSelected: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Find and click the CPU hardware option
|
||||
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
||||
// CPU is the button with "CPU" text
|
||||
for (const button of hardwareButtons) {
|
||||
if (button.textContent?.includes('CPU')) {
|
||||
button.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with manual install selected
|
||||
export const ManualInstall: Story = {
|
||||
render: () => ({
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Find and click the Manual Install hardware option
|
||||
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
|
||||
// Manual Install is the button with "Manual Install" text
|
||||
for (const button of hardwareButtons) {
|
||||
if (button.textContent?.includes('Manual Install')) {
|
||||
button.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
})
|
||||
}
|
||||
|
||||
// Story with error state (invalid install path)
|
||||
export const ErrorState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return an error
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: false,
|
||||
exists: false,
|
||||
canWrite: false,
|
||||
freeSpace: 100000000000,
|
||||
requiredSpace: 10000000000,
|
||||
isNonDefaultDrive: false,
|
||||
error: 'Story mock: Example error state'
|
||||
})
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll(
|
||||
'.p-selectbutton-option'
|
||||
)
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2 where error will be shown
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story with warning state (non-default drive)
|
||||
export const WarningState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return a warning about non-default drive
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
canWrite: true,
|
||||
freeSpace: 500_000_000_000,
|
||||
requiredSpace: 10_000_000_000,
|
||||
isNonDefaultDrive: true
|
||||
})
|
||||
|
||||
return {
|
||||
components: { InstallView },
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
async mounted() {
|
||||
// Wait for component to be fully mounted
|
||||
await nextTick()
|
||||
|
||||
// Select Apple Metal option to enable navigation
|
||||
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
|
||||
if (hardwareOptions.length > 0) {
|
||||
hardwareOptions[0].click() // Click Apple Metal (first option)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click Next to go to step 2 where warning will be shown
|
||||
const buttons = Array.from(
|
||||
this.$el.querySelectorAll('button')
|
||||
) as HTMLButtonElement[]
|
||||
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
|
||||
if (nextBtn) {
|
||||
nextBtn.click()
|
||||
}
|
||||
},
|
||||
template: '<InstallView />'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,54 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<!-- h-full to make sure the stepper does not layout shift between steps
|
||||
as for each step the stepper height is different. Inherit the center element
|
||||
placement from BaseViewTemplate would cause layout shift. -->
|
||||
<Stepper
|
||||
class="h-full p-8 2xl:p-16"
|
||||
value="0"
|
||||
@update:value="handleStepChange"
|
||||
>
|
||||
<StepList class="select-none">
|
||||
<Step value="0">
|
||||
{{ $t('install.gpu') }}
|
||||
</Step>
|
||||
<Step value="1" :disabled="noGpu">
|
||||
{{ $t('install.installLocation') }}
|
||||
</Step>
|
||||
<Step value="2" :disabled="noGpu || hasError || highestStep < 1">
|
||||
{{ $t('install.migration') }}
|
||||
</Step>
|
||||
<Step value="3" :disabled="noGpu || hasError || highestStep < 2">
|
||||
{{ $t('install.desktopSettings') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
<StepPanels>
|
||||
<StepPanel v-slot="{ activateCallback }" value="0">
|
||||
<GpuPicker v-model:device="device" />
|
||||
<div class="flex pt-6 justify-end">
|
||||
<Button
|
||||
:label="$t('g.next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
:disabled="typeof device !== 'string'"
|
||||
@click="activateCallback('1')"
|
||||
<!-- Fixed height container with flexbox layout for proper content management -->
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<Stepper
|
||||
v-model:value="currentStep"
|
||||
class="flex flex-col h-full"
|
||||
@update:value="handleStepChange"
|
||||
>
|
||||
<!-- Main content area that grows to fill available space -->
|
||||
<StepPanels
|
||||
class="flex-1 overflow-auto"
|
||||
:style="{ scrollbarGutter: 'stable' }"
|
||||
>
|
||||
<StepPanel value="1" class="flex">
|
||||
<GpuPicker v-model:device="device" />
|
||||
</StepPanel>
|
||||
<StepPanel value="2">
|
||||
<InstallLocationPicker
|
||||
v-model:install-path="installPath"
|
||||
v-model:path-error="pathError"
|
||||
v-model:migration-source-path="migrationSourcePath"
|
||||
v-model:migration-item-ids="migrationItemIds"
|
||||
v-model:python-mirror="pythonMirror"
|
||||
v-model:pypi-mirror="pypiMirror"
|
||||
v-model:torch-mirror="torchMirror"
|
||||
:device="device"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback }" value="1">
|
||||
<InstallLocationPicker
|
||||
v-model:installPath="installPath"
|
||||
v-model:pathError="pathError"
|
||||
/>
|
||||
<div class="flex pt-6 justify-between">
|
||||
<Button
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="activateCallback('0')"
|
||||
</StepPanel>
|
||||
<StepPanel value="3">
|
||||
<DesktopSettingsConfiguration
|
||||
v-model:auto-update="autoUpdate"
|
||||
v-model:allow-metrics="allowMetrics"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
:disabled="pathError !== ''"
|
||||
@click="activateCallback('2')"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback }" value="2">
|
||||
<MigrationPicker
|
||||
v-model:sourcePath="migrationSourcePath"
|
||||
v-model:migrationItemIds="migrationItemIds"
|
||||
/>
|
||||
<div class="flex pt-6 justify-between">
|
||||
<Button
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="activateCallback('1')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
@click="activateCallback('3')"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback }" value="3">
|
||||
<DesktopSettingsConfiguration
|
||||
v-model:autoUpdate="autoUpdate"
|
||||
v-model:allowMetrics="allowMetrics"
|
||||
/>
|
||||
<MirrorsConfiguration
|
||||
v-model:pythonMirror="pythonMirror"
|
||||
v-model:pypiMirror="pypiMirror"
|
||||
v-model:torchMirror="torchMirror"
|
||||
:device="device"
|
||||
class="mt-6"
|
||||
/>
|
||||
<div class="flex mt-6 justify-between">
|
||||
<Button
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="activateCallback('2')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('g.install')"
|
||||
icon="pi pi-check"
|
||||
icon-pos="right"
|
||||
:disabled="hasError"
|
||||
@click="install()"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
|
||||
<!-- Install footer with navigation -->
|
||||
<InstallFooter
|
||||
class="w-full max-w-2xl my-6 mx-auto"
|
||||
:current-step
|
||||
:can-proceed
|
||||
:disable-location-step="noGpu"
|
||||
:disable-migration-step="noGpu || hasError || highestStep < 2"
|
||||
:disable-settings-step="noGpu || hasError || highestStep < 3"
|
||||
@previous="goToPreviousStep"
|
||||
@next="goToNextStep"
|
||||
@install="install"
|
||||
/>
|
||||
</Stepper>
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
@@ -114,9 +57,6 @@ import type {
|
||||
InstallOptions,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Button from 'primevue/button'
|
||||
import Step from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
import StepPanel from 'primevue/steppanel'
|
||||
import StepPanels from 'primevue/steppanels'
|
||||
import Stepper from 'primevue/stepper'
|
||||
@@ -125,9 +65,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
|
||||
import GpuPicker from '@/components/install/GpuPicker.vue'
|
||||
import InstallFooter from '@/components/install/InstallFooter.vue'
|
||||
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import MirrorsConfiguration from '@/components/install/MirrorsConfiguration.vue'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
@@ -145,6 +84,9 @@ const pythonMirror = ref('')
|
||||
const pypiMirror = ref('')
|
||||
const torchMirror = ref('')
|
||||
|
||||
/** Current step in the stepper */
|
||||
const currentStep = ref('1')
|
||||
|
||||
/** Forces each install step to be visited at least once. */
|
||||
const highestStep = ref(0)
|
||||
|
||||
@@ -164,6 +106,40 @@ const setHighestStep = (value: string | number) => {
|
||||
const hasError = computed(() => pathError.value !== '')
|
||||
const noGpu = computed(() => typeof device.value !== 'string')
|
||||
|
||||
// Computed property to determine if user can proceed to next step
|
||||
const regex = /^Insufficient space - minimum free space: \d+ GB$/
|
||||
|
||||
const canProceed = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
case '1':
|
||||
return typeof device.value === 'string'
|
||||
case '2':
|
||||
return pathError.value === '' || regex.test(pathError.value)
|
||||
case '3':
|
||||
return !hasError.value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Navigation methods
|
||||
const goToNextStep = () => {
|
||||
const nextStep = (parseInt(currentStep.value) + 1).toString()
|
||||
currentStep.value = nextStep
|
||||
setHighestStep(nextStep)
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
step: nextStep
|
||||
})
|
||||
}
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
const prevStep = (parseInt(currentStep.value) - 1).toString()
|
||||
currentStep.value = prevStep
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
step: prevStep
|
||||
})
|
||||
}
|
||||
|
||||
const electron = electronAPI()
|
||||
const router = useRouter()
|
||||
const install = async () => {
|
||||
@@ -195,14 +171,40 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
step: '0',
|
||||
step: currentStep.value,
|
||||
gpu: detectedGpu
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
:deep(.p-steppanel) {
|
||||
@apply mt-8 flex justify-center bg-transparent;
|
||||
}
|
||||
|
||||
/* Remove default padding/margin from StepPanels to make scrollbar flush */
|
||||
:deep(.p-steppanels) {
|
||||
@apply p-0 m-0;
|
||||
}
|
||||
|
||||
/* Ensure StepPanel content container has no top/bottom padding */
|
||||
:deep(.p-steppanel-content) {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
|
||||
:deep(.p-steppanels::-webkit-scrollbar) {
|
||||
@apply w-4;
|
||||
}
|
||||
|
||||
:deep(.p-steppanels::-webkit-scrollbar-track) {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
|
||||
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div
|
||||
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme overflow-y-auto"
|
||||
>
|
||||
<div class="max-w-screen-sm w-screen m-8 relative">
|
||||
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
|
||||
<!-- Header -->
|
||||
<h1 class="backspan pi-wrench text-4xl font-bold">
|
||||
{{ t('maintenance.title') }}
|
||||
@@ -100,7 +100,7 @@ import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
|
||||
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
|
||||
import type { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
@@ -178,6 +178,8 @@ onUnmounted(() => electron.Validation.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
:deep(.p-tag) {
|
||||
--p-tag-gap: 0.375rem;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ const continueToInstall = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.sad-container {
|
||||
@apply grid items-center justify-evenly;
|
||||
grid-template-columns: 25rem 1fr;
|
||||
|
||||
@@ -1,75 +1,205 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="flex-col">
|
||||
<div class="flex flex-col w-full h-full items-center">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
class="flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex items-center my-4 gap-2">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-wrench"
|
||||
:label="t('serverStart.troubleshoot')"
|
||||
@click="troubleshoot"
|
||||
/>
|
||||
<BaseViewTemplate dark>
|
||||
<div class="relative min-h-screen">
|
||||
<!-- Terminal Background Layer (always visible during loading) -->
|
||||
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
|
||||
<div class="h-full w-full">
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Semi-transparent overlay -->
|
||||
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
|
||||
|
||||
<!-- Smooth radial gradient overlay -->
|
||||
<div
|
||||
v-if="!isError"
|
||||
class="fixed inset-0 z-8"
|
||||
style="
|
||||
background: radial-gradient(
|
||||
ellipse 800px 600px at center,
|
||||
rgba(23, 23, 23, 0.95) 0%,
|
||||
rgba(23, 23, 23, 0.93) 10%,
|
||||
rgba(23, 23, 23, 0.9) 20%,
|
||||
rgba(23, 23, 23, 0.85) 30%,
|
||||
rgba(23, 23, 23, 0.75) 40%,
|
||||
rgba(23, 23, 23, 0.6) 50%,
|
||||
rgba(23, 23, 23, 0.4) 60%,
|
||||
rgba(23, 23, 23, 0.2) 70%,
|
||||
rgba(23, 23, 23, 0.1) 80%,
|
||||
rgba(23, 23, 23, 0.05) 90%,
|
||||
transparent 100%
|
||||
);
|
||||
"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<!-- Main startup display using StartupDisplay component -->
|
||||
<StartupDisplay
|
||||
:title="displayTitle"
|
||||
:status-text="displayStatusText"
|
||||
:progress-percentage="installStageProgress"
|
||||
:hide-progress="isError"
|
||||
/>
|
||||
|
||||
<!-- Error Section (positioned at bottom) -->
|
||||
<div
|
||||
v-if="isError"
|
||||
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
:label="$t('serverStart.reportIssue')"
|
||||
severity="secondary"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
:label="$t('serverStart.openLogs')"
|
||||
severity="secondary"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-wrench"
|
||||
:label="$t('serverStart.troubleshoot')"
|
||||
@click="troubleshoot"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button
|
||||
v-if="!terminalVisible"
|
||||
class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-2 mx-auto"
|
||||
@click="terminalVisible = true"
|
||||
>
|
||||
<i class="pi pi-search"></i>
|
||||
{{ $t('serverStart.showTerminal') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
||||
<div
|
||||
v-if="terminalVisible && isError"
|
||||
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
|
||||
>
|
||||
<div
|
||||
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
|
||||
>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!terminalVisible"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.showTerminal')"
|
||||
@click="terminalVisible = true"
|
||||
/>
|
||||
</div>
|
||||
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ProgressStatus } from '@comfyorg/comfyui-electron-types'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import {
|
||||
InstallStage,
|
||||
type InstallStageInfo,
|
||||
type InstallStageName,
|
||||
ProgressStatus
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import Button from 'primevue/button'
|
||||
import { Ref, onMounted, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
||||
import StartupDisplay from '@/components/common/StartupDisplay.vue'
|
||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const electron = electronAPI()
|
||||
const { t } = useI18n()
|
||||
const electron = electronAPI()
|
||||
|
||||
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||
const electronVersion = ref<string>('')
|
||||
const terminalVisible = ref(false)
|
||||
|
||||
const installStage = ref<InstallStageName | null>(null)
|
||||
const installStageMessage = ref<string>('')
|
||||
const installStageProgress = ref<number | undefined>(undefined)
|
||||
|
||||
let xterm: Terminal | undefined
|
||||
|
||||
const terminalVisible = ref(true)
|
||||
/**
|
||||
* Handles installation stage updates from the desktop
|
||||
*/
|
||||
const updateInstallStage = (stageInfo: InstallStageInfo) => {
|
||||
console.warn('[InstallStage.onUpdate] Received:', {
|
||||
stage: stageInfo.stage,
|
||||
progress: stageInfo.progress,
|
||||
message: stageInfo.message,
|
||||
error: stageInfo.error,
|
||||
timestamp: stageInfo.timestamp,
|
||||
fullInfo: stageInfo
|
||||
})
|
||||
|
||||
installStage.value = stageInfo.stage
|
||||
installStageMessage.value = stageInfo.message || ''
|
||||
installStageProgress.value = stageInfo.progress
|
||||
}
|
||||
|
||||
const currentStatusLabel = computed(() => {
|
||||
// Use the message from the Electron API if available
|
||||
if (installStageMessage.value) {
|
||||
return installStageMessage.value
|
||||
}
|
||||
return t(`serverStart.process.${status.value}`)
|
||||
})
|
||||
|
||||
const isError = computed(
|
||||
() =>
|
||||
status.value === ProgressStatus.ERROR ||
|
||||
installStage.value === InstallStage.ERROR
|
||||
)
|
||||
|
||||
const isInstallationStage = computed(() => {
|
||||
const installationStages: InstallStageName[] = [
|
||||
InstallStage.WELCOME_SCREEN,
|
||||
InstallStage.INSTALL_OPTIONS_SELECTION,
|
||||
InstallStage.CREATING_DIRECTORIES,
|
||||
InstallStage.INITIALIZING_CONFIG,
|
||||
InstallStage.PYTHON_ENVIRONMENT_SETUP,
|
||||
InstallStage.INSTALLING_REQUIREMENTS,
|
||||
InstallStage.INSTALLING_PYTORCH,
|
||||
InstallStage.INSTALLING_COMFYUI_REQUIREMENTS,
|
||||
InstallStage.INSTALLING_MANAGER_REQUIREMENTS,
|
||||
InstallStage.MIGRATING_CUSTOM_NODES
|
||||
]
|
||||
return (
|
||||
installStage.value !== null &&
|
||||
installationStages.includes(installStage.value)
|
||||
)
|
||||
})
|
||||
|
||||
const displayTitle = computed(() => {
|
||||
if (isError.value) {
|
||||
return t('serverStart.errorMessage')
|
||||
}
|
||||
if (isInstallationStage.value) {
|
||||
return t('serverStart.installation.title')
|
||||
}
|
||||
return t('serverStart.title')
|
||||
})
|
||||
|
||||
const displayStatusText = computed(() => {
|
||||
if (isError.value && electronVersion.value) {
|
||||
return `v${electronVersion.value}`
|
||||
}
|
||||
return currentStatusLabel.value
|
||||
})
|
||||
|
||||
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||
status.value = newStatus
|
||||
|
||||
// Make critical error screen more obvious.
|
||||
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
|
||||
else xterm?.clear()
|
||||
}
|
||||
|
||||
const terminalCreated = (
|
||||
@@ -94,16 +224,30 @@ const reportIssue = () => {
|
||||
}
|
||||
const openLogs = () => electron.openLogsFolder()
|
||||
|
||||
let cleanupInstallStageListener: (() => void) | undefined
|
||||
|
||||
onMounted(async () => {
|
||||
electron.sendReady()
|
||||
electron.onProgressUpdate(updateProgress)
|
||||
cleanupInstallStageListener =
|
||||
electron.InstallStage.onUpdate(updateInstallStage)
|
||||
|
||||
const stageInfo = await electron.InstallStage.getCurrent()
|
||||
updateInstallStage(stageInfo)
|
||||
electronVersion.value = await electron.getElectronVersion()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
xterm?.dispose()
|
||||
cleanupInstallStageListener?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.xterm-helper-textarea) {
|
||||
/* Hide this as it moves all over when uv is running */
|
||||
display: none;
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
/* Hide the xterm scrollbar completely */
|
||||
:deep(.p-terminal) .xterm-viewport {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<BaseViewTemplate dark>
|
||||
<main
|
||||
id="comfy-user-selection"
|
||||
class="min-w-84 relative rounded-lg bg-[var(--comfy-menu-bg)] p-5 px-10 shadow-lg"
|
||||
class="min-w-84 relative rounded-lg bg-(--comfy-menu-bg) p-5 px-10 shadow-lg"
|
||||
>
|
||||
<h1 class="my-2.5 mb-7 font-normal">ComfyUI</h1>
|
||||
<div class="flex w-full flex-col items-center">
|
||||
@@ -50,7 +50,8 @@ import Select from 'primevue/select'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { User, useUserStore } from '@/stores/userStore'
|
||||
import type { User } from '@/stores/userStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<h1 class="animated-gradient-text text-glow select-none">
|
||||
{{ $t('welcome.title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Get Started Button -->
|
||||
<Button
|
||||
:label="$t('welcome.getStarted')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
size="large"
|
||||
rounded
|
||||
class="p-4 text-lg fade-in-up"
|
||||
@click="navigateTo('/install')"
|
||||
/>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="grid grid-rows-2 gap-8">
|
||||
<!-- Top container: Logo -->
|
||||
<div class="flex items-end justify-center">
|
||||
<img
|
||||
src="/assets/images/comfy-brand-mark.svg"
|
||||
:alt="$t('g.logoAlt')"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
<!-- Bottom container: Title and button -->
|
||||
<div class="flex flex-col items-center justify-center gap-4">
|
||||
<Button
|
||||
:label="$t('welcome.getStarted')"
|
||||
class="px-8 mt-4 bg-brand-yellow hover:bg-brand-yellow/90 border-0 rounded-lg transition-colors"
|
||||
:pt="{
|
||||
label: { class: 'font-inter text-neutral-900 font-black' }
|
||||
}"
|
||||
@click="navigateTo('/install')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
@@ -31,47 +37,3 @@ const navigateTo = async (path: string) => {
|
||||
await router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animated-gradient-text {
|
||||
@apply font-bold;
|
||||
font-size: clamp(2rem, 8vw, 4rem);
|
||||
background: linear-gradient(to right, #12c2e9, #c471ed, #f64f59, #12c2e9);
|
||||
background-size: 300% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient 8s linear infinite;
|
||||
}
|
||||
|
||||
.text-glow {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% center;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 300% center;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 1.5s ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
<div
|
||||
v-show="isNativeWindow()"
|
||||
ref="topMenuRef"
|
||||
class="app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
class="app-drag w-full h-(--comfy-topbar-height)"
|
||||
/>
|
||||
<div
|
||||
class="flex-1 flex-grow w-full flex items-center justify-center overflow-auto"
|
||||
>
|
||||
<div class="grow w-full flex items-center justify-center overflow-auto">
|
||||
<slot />
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
|
||||
Reference in New Issue
Block a user