mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
Rework desktop install / startup UX (#5292)
### Summary Complete redesign of Desktop app installer, implementing the new app startup progress system and error reporting.
This commit is contained in:
3
public/assets/images/comfy-brand-mark.svg
Normal file
3
public/assets/images/comfy-brand-mark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/assets/images/nvidia-logo-square.jpg
Normal file
BIN
public/assets/images/nvidia-logo-square.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
|
<div
|
||||||
|
ref="rootEl"
|
||||||
|
class="relative overflow-hidden h-full w-full bg-neutral-900"
|
||||||
|
>
|
||||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||||
<div ref="terminalEl" class="h-full terminal-host" />
|
<div ref="terminalEl" class="h-full terminal-host" />
|
||||||
</div>
|
</div>
|
||||||
@@ -98,12 +101,13 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference '../../../../assets/css/style.css';
|
||||||
|
|
||||||
:deep(.p-terminal) .xterm {
|
:deep(.p-terminal) .xterm {
|
||||||
overflow-x: auto;
|
@apply overflow-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-terminal) .xterm-screen {
|
:deep(.p-terminal) .xterm-screen {
|
||||||
background-color: black;
|
@apply bg-neutral-900 overflow-hidden;
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
71
src/components/common/StartupDisplay.vue
Normal file
71
src/components/common/StartupDisplay.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="wrapperClass">
|
||||||
|
<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: Progress and text -->
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4">
|
||||||
|
<ProgressBar
|
||||||
|
v-if="!hideProgress"
|
||||||
|
:mode="progressMode"
|
||||||
|
:value="progressPercentage ?? 0"
|
||||||
|
:show-value="false"
|
||||||
|
class="w-90 h-2 mt-8"
|
||||||
|
:pt="{ value: { class: 'bg-brand-yellow' } }"
|
||||||
|
/>
|
||||||
|
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<p v-if="statusText" class="text-lg text-neutral-400">
|
||||||
|
{{ statusText }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ProgressBar from 'primevue/progressbar'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
/** Props for the StartupDisplay component */
|
||||||
|
interface StartupDisplayProps {
|
||||||
|
/** Progress: 0-100 for determinate, undefined for indeterminate */
|
||||||
|
progressPercentage?: number
|
||||||
|
/** Main title text */
|
||||||
|
title?: string
|
||||||
|
/** Status text shown below the title */
|
||||||
|
statusText?: string
|
||||||
|
/** Hide the progress bar */
|
||||||
|
hideProgress?: boolean
|
||||||
|
/** Use full screen wrapper (default: true) */
|
||||||
|
fullScreen?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
progressPercentage,
|
||||||
|
title,
|
||||||
|
statusText,
|
||||||
|
hideProgress = false,
|
||||||
|
fullScreen = true
|
||||||
|
} = defineProps<StartupDisplayProps>()
|
||||||
|
|
||||||
|
const progressMode = computed(() =>
|
||||||
|
progressPercentage === undefined ? 'indeterminate' : 'determinate'
|
||||||
|
)
|
||||||
|
|
||||||
|
const wrapperClass = computed(() =>
|
||||||
|
fullScreen
|
||||||
|
? 'flex items-center justify-center min-h-screen'
|
||||||
|
: 'flex items-center justify-center'
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg">
|
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg text-sm">
|
||||||
<!-- Auto Update Setting -->
|
<!-- Auto Update Setting -->
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-lg font-medium text-neutral-100">
|
<h3 class="text-lg font-medium text-neutral-100">
|
||||||
{{ $t('install.settings.autoUpdate') }}
|
{{ $t('install.settings.autoUpdate') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-neutral-400 mt-1">
|
<p class="text-neutral-400 mt-1">
|
||||||
{{ $t('install.settings.autoUpdateDescription') }}
|
{{ $t('install.settings.autoUpdateDescription') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,14 +32,10 @@
|
|||||||
<h3 class="text-lg font-medium text-neutral-100">
|
<h3 class="text-lg font-medium text-neutral-100">
|
||||||
{{ $t('install.settings.allowMetrics') }}
|
{{ $t('install.settings.allowMetrics') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-neutral-400 mt-1">
|
<p class="text-neutral-400">
|
||||||
{{ $t('install.settings.allowMetricsDescription') }}
|
{{ $t('install.settings.allowMetricsDescription') }}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a href="#" @click.prevent="showMetricsInfo">
|
||||||
href="#"
|
|
||||||
class="text-sm text-blue-400 hover:text-blue-300 mt-1 inline-block"
|
|
||||||
@click.prevent="showMetricsInfo"
|
|
||||||
>
|
|
||||||
{{ $t('install.settings.learnMoreAboutData') }}
|
{{ $t('install.settings.learnMoreAboutData') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +47,9 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
v-model:visible="showDialog"
|
v-model:visible="showDialog"
|
||||||
modal
|
modal
|
||||||
|
dismissable-mask
|
||||||
:header="$t('install.settings.dataCollectionDialog.title')"
|
:header="$t('install.settings.dataCollectionDialog.title')"
|
||||||
|
class="select-none"
|
||||||
>
|
>
|
||||||
<div class="text-neutral-300">
|
<div class="text-neutral-300">
|
||||||
<h4 class="font-medium mb-2">
|
<h4 class="font-medium mb-2">
|
||||||
@@ -110,11 +108,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a
|
<a href="https://comfy.org/privacy" target="_blank">
|
||||||
href="https://comfy.org/privacy"
|
|
||||||
target="_blank"
|
|
||||||
class="text-blue-400 hover:text-blue-300 underline"
|
|
||||||
>
|
|
||||||
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
|
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,126 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-6 w-[600px] h-[30rem] select-none">
|
<div
|
||||||
<!-- Installation Path Section -->
|
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
|
||||||
<div class="grow flex flex-col gap-4 text-neutral-300">
|
>
|
||||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||||
{{ $t('install.gpuSelection.selectGpu') }}
|
{{ $t('install.gpuPicker.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="m-1 text-neutral-400">
|
<!-- GPU Selection buttons - takes up remaining space and centers content -->
|
||||||
{{ $t('install.gpuSelection.selectGpuDescription') }}:
|
<div class="flex-1 flex gap-8 justify-center items-center">
|
||||||
</p>
|
<!-- Apple Metal / NVIDIA -->
|
||||||
|
<HardwareOption
|
||||||
|
v-if="platform === 'darwin'"
|
||||||
|
:image-path="'/assets/images/apple-mps-logo.png'"
|
||||||
|
placeholder-text="Apple Metal"
|
||||||
|
subtitle="Apple Metal"
|
||||||
|
:value="'mps'"
|
||||||
|
:selected="selected === 'mps'"
|
||||||
|
:recommended="true"
|
||||||
|
@click="pickGpu('mps')"
|
||||||
|
/>
|
||||||
|
<HardwareOption
|
||||||
|
v-else
|
||||||
|
:image-path="'/assets/images/nvidia-logo-square.jpg'"
|
||||||
|
placeholder-text="NVIDIA"
|
||||||
|
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
|
||||||
|
:value="'nvidia'"
|
||||||
|
:selected="selected === 'nvidia'"
|
||||||
|
:recommended="true"
|
||||||
|
@click="pickGpu('nvidia')"
|
||||||
|
/>
|
||||||
|
<!-- CPU -->
|
||||||
|
<HardwareOption
|
||||||
|
placeholder-text="CPU"
|
||||||
|
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
|
||||||
|
:value="'cpu'"
|
||||||
|
:selected="selected === 'cpu'"
|
||||||
|
@click="pickGpu('cpu')"
|
||||||
|
/>
|
||||||
|
<!-- Manual Install -->
|
||||||
|
<HardwareOption
|
||||||
|
placeholder-text="Manual Install"
|
||||||
|
:subtitle="$t('install.gpuPicker.manualSubtitle')"
|
||||||
|
:value="'unsupported'"
|
||||||
|
:selected="selected === 'unsupported'"
|
||||||
|
@click="pickGpu('unsupported')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- GPU Selection buttons -->
|
<div class="pt-12 px-24 h-16">
|
||||||
<div
|
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
|
||||||
class="flex gap-2 text-center transition-opacity"
|
<Tag
|
||||||
:class="{ selected: selected }"
|
:value="$t('install.gpuPicker.recommended')"
|
||||||
>
|
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
|
||||||
<!-- NVIDIA -->
|
/>
|
||||||
<div
|
<i-lucide:badge-check class="text-neutral-300 text-lg" />
|
||||||
v-if="platform !== 'darwin'"
|
|
||||||
class="gpu-button"
|
|
||||||
:class="{ selected: selected === 'nvidia' }"
|
|
||||||
role="button"
|
|
||||||
@click="pickGpu('nvidia')"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="m-12"
|
|
||||||
alt="NVIDIA logo"
|
|
||||||
width="196"
|
|
||||||
height="32"
|
|
||||||
src="/assets/images/nvidia-logo.svg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- MPS -->
|
|
||||||
<div
|
|
||||||
v-if="platform === 'darwin'"
|
|
||||||
class="gpu-button"
|
|
||||||
:class="{ selected: selected === 'mps' }"
|
|
||||||
role="button"
|
|
||||||
@click="pickGpu('mps')"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="rounded-lg hover-brighten"
|
|
||||||
alt="Apple Metal Performance Shaders Logo"
|
|
||||||
width="292"
|
|
||||||
ratio
|
|
||||||
src="/assets/images/apple-mps-logo.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- Manual configuration -->
|
|
||||||
<div
|
|
||||||
class="gpu-button"
|
|
||||||
:class="{ selected: selected === 'unsupported' }"
|
|
||||||
role="button"
|
|
||||||
@click="pickGpu('unsupported')"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="m-12"
|
|
||||||
alt="Manual configuration"
|
|
||||||
width="196"
|
|
||||||
src="/assets/images/manual-configuration.svg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Details on selected GPU -->
|
|
||||||
<p v-if="selected === 'nvidia'" class="m-1">
|
|
||||||
<Tag icon="pi pi-check" severity="success" :value="'CUDA'" />
|
|
||||||
{{ $t('install.gpuSelection.nvidiaDescription') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="selected === 'mps'" class="m-1">
|
|
||||||
<Tag icon="pi pi-check" severity="success" :value="'MPS'" />
|
|
||||||
{{ $t('install.gpuSelection.mpsDescription') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-if="selected === 'unsupported'" class="text-neutral-300">
|
|
||||||
<p class="m-1">
|
|
||||||
<Tag
|
|
||||||
icon="pi pi-exclamation-triangle"
|
|
||||||
severity="warn"
|
|
||||||
:value="t('icon.exclamation-triangle')"
|
|
||||||
/>
|
|
||||||
{{ $t('install.gpuSelection.customSkipsPython') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>
|
|
||||||
{{ $t('install.gpuSelection.customComfyNeedsPython') }}
|
|
||||||
</strong>
|
|
||||||
</li>
|
|
||||||
<li>{{ $t('install.gpuSelection.customManualVenv') }}</li>
|
|
||||||
<li>{{ $t('install.gpuSelection.customInstallRequirements') }}</li>
|
|
||||||
<li>{{ $t('install.gpuSelection.customMayNotWork') }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selected === 'cpu'">
|
|
||||||
<p class="m-1">
|
|
||||||
<Tag
|
|
||||||
icon="pi pi-exclamation-triangle"
|
|
||||||
severity="warn"
|
|
||||||
:value="t('icon.exclamation-triangle')"
|
|
||||||
/>
|
|
||||||
{{ $t('install.gpuSelection.cpuModeDescription') }}
|
|
||||||
</p>
|
|
||||||
<p class="m-1">
|
|
||||||
{{ $t('install.gpuSelection.cpuModeDescription2') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="text-neutral-300 px-24">
|
||||||
class="transition-opacity flex gap-3 h-0 items-center"
|
<p v-show="descriptionText" class="leading-relaxed">
|
||||||
:class="{
|
{{ descriptionText }}
|
||||||
'opacity-40': selected && selected !== 'cpu'
|
</p>
|
||||||
}"
|
|
||||||
>
|
|
||||||
<ToggleSwitch v-model="cpuMode" input-id="cpu-mode" />
|
|
||||||
<label for="cpu-mode" class="select-none">
|
|
||||||
{{ $t('install.gpuSelection.enableCpuMode') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -128,20 +68,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
import ToggleSwitch from 'primevue/toggleswitch'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
|
import HardwareOption from '@/components/install/HardwareOption.vue'
|
||||||
|
import { st } from '@/i18n'
|
||||||
import { electronAPI } from '@/utils/envUtil'
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const cpuMode = computed({
|
|
||||||
get: () => selected.value === 'cpu',
|
|
||||||
set: (value) => {
|
|
||||||
selected.value = value ? 'cpu' : null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const selected = defineModel<TorchDeviceType | null>('device', {
|
const selected = defineModel<TorchDeviceType | null>('device', {
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
@@ -149,55 +81,23 @@ const selected = defineModel<TorchDeviceType | null>('device', {
|
|||||||
const electron = electronAPI()
|
const electron = electronAPI()
|
||||||
const platform = electron.getPlatform()
|
const platform = electron.getPlatform()
|
||||||
|
|
||||||
const pickGpu = (value: typeof selected.value) => {
|
const showRecommendedBadge = computed(
|
||||||
const newValue = selected.value === value ? null : value
|
() => selected.value === 'mps' || selected.value === 'nvidia'
|
||||||
selected.value = newValue
|
)
|
||||||
|
|
||||||
|
const descriptionKeys = {
|
||||||
|
mps: 'appleMetal',
|
||||||
|
nvidia: 'nvidia',
|
||||||
|
cpu: 'cpu',
|
||||||
|
unsupported: 'manual'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const descriptionText = computed(() => {
|
||||||
|
const key = selected.value ? descriptionKeys[selected.value] : undefined
|
||||||
|
return st(`install.gpuPicker.${key}Description`, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
const pickGpu = (value: TorchDeviceType) => {
|
||||||
|
selected.value = value
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference '../../assets/css/style.css';
|
|
||||||
|
|
||||||
.p-tag {
|
|
||||||
--p-tag-gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-brighten {
|
|
||||||
@apply transition-colors;
|
|
||||||
transition-property: filter, box-shadow;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
filter: brightness(107%) contrast(105%);
|
|
||||||
box-shadow: 0 0 0.25rem #ffffff79;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.p-accordioncontent-content {
|
|
||||||
@apply bg-neutral-900 rounded-lg transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.selected {
|
|
||||||
.gpu-button:not(.selected) {
|
|
||||||
@apply opacity-50 hover:opacity-100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gpu-button {
|
|
||||||
@apply w-1/2 m-0 cursor-pointer rounded-lg flex flex-col items-center justify-around bg-neutral-800/50 hover:bg-neutral-800/75 transition-colors;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
@apply opacity-100 bg-neutral-700/50 hover:bg-neutral-700/60;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
@apply pointer-events-none opacity-40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-card-header {
|
|
||||||
@apply text-center grow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-card-body {
|
|
||||||
@apply text-center pt-0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
73
src/components/install/HardwareOption.stories.ts
Normal file
73
src/components/install/HardwareOption.stories.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// eslint-disable-next-line storybook/no-renderer-packages
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||||
|
|
||||||
|
import HardwareOption from './HardwareOption.vue'
|
||||||
|
|
||||||
|
const meta: Meta<typeof HardwareOption> = {
|
||||||
|
title: 'Desktop/Components/HardwareOption',
|
||||||
|
component: HardwareOption,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
backgrounds: {
|
||||||
|
default: 'dark',
|
||||||
|
values: [{ name: 'dark', value: '#1a1a1a' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
selected: { control: 'boolean' },
|
||||||
|
imagePath: { control: 'text' },
|
||||||
|
placeholderText: { control: 'text' },
|
||||||
|
subtitle: { control: 'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const AppleMetalSelected: Story = {
|
||||||
|
args: {
|
||||||
|
imagePath: '/assets/images/apple-mps-logo.png',
|
||||||
|
placeholderText: 'Apple Metal',
|
||||||
|
subtitle: 'Apple Metal',
|
||||||
|
value: 'mps',
|
||||||
|
selected: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppleMetalUnselected: Story = {
|
||||||
|
args: {
|
||||||
|
imagePath: '/assets/images/apple-mps-logo.png',
|
||||||
|
placeholderText: 'Apple Metal',
|
||||||
|
subtitle: 'Apple Metal',
|
||||||
|
value: 'mps',
|
||||||
|
selected: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CPUOption: Story = {
|
||||||
|
args: {
|
||||||
|
placeholderText: 'CPU',
|
||||||
|
subtitle: 'Subtitle',
|
||||||
|
value: 'cpu',
|
||||||
|
selected: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ManualInstall: Story = {
|
||||||
|
args: {
|
||||||
|
placeholderText: 'Manual Install',
|
||||||
|
subtitle: 'Subtitle',
|
||||||
|
value: 'unsupported',
|
||||||
|
selected: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NvidiaSelected: Story = {
|
||||||
|
args: {
|
||||||
|
imagePath: '/assets/images/nvidia-logo-square.jpg',
|
||||||
|
placeholderText: 'NVIDIA',
|
||||||
|
subtitle: 'NVIDIA',
|
||||||
|
value: 'nvidia',
|
||||||
|
selected: true
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/components/install/HardwareOption.vue
Normal file
55
src/components/install/HardwareOption.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Recommended Badge -->
|
||||||
|
<button
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'hardware-option w-[170px] h-[190px] p-5 flex flex-col items-center rounded-3xl transition-all duration-200 bg-neutral-900/70 border-4',
|
||||||
|
selected ? 'border-solid border-brand-yellow' : 'border-transparent'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<!-- Icon/Logo Area - Rounded square container -->
|
||||||
|
<div
|
||||||
|
class="icon-container w-[110px] h-[110px] shrink-0 rounded-2xl bg-neutral-800 flex items-center justify-center overflow-hidden"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="imagePath"
|
||||||
|
:src="imagePath"
|
||||||
|
:alt="placeholderText"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
style="object-position: 57% center"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-xl font-medium text-neutral-400">
|
||||||
|
{{ placeholderText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Content -->
|
||||||
|
<div v-if="subtitle" class="text-center mt-4">
|
||||||
|
<div class="text-sm text-neutral-500">{{ subtitle }}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
imagePath?: string
|
||||||
|
placeholderText: string
|
||||||
|
subtitle?: string
|
||||||
|
value: TorchDeviceType
|
||||||
|
selected?: boolean
|
||||||
|
recommended?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
defineEmits<{ click: [] }>()
|
||||||
|
</script>
|
||||||
79
src/components/install/InstallFooter.vue
Normal file
79
src/components/install/InstallFooter.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-4">
|
||||||
|
<!-- Back button -->
|
||||||
|
<Button
|
||||||
|
v-if="currentStep !== '1'"
|
||||||
|
:label="$t('g.back')"
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-arrow-left"
|
||||||
|
class="font-inter rounded-lg border-0 px-6 py-2 justify-self-start"
|
||||||
|
@click="$emit('previous')"
|
||||||
|
/>
|
||||||
|
<div v-else></div>
|
||||||
|
|
||||||
|
<!-- Step indicators in center -->
|
||||||
|
<StepList class="flex justify-center items-center gap-3 select-none">
|
||||||
|
<Step value="1" :pt="stepPassthrough">
|
||||||
|
{{ $t('install.gpu') }}
|
||||||
|
</Step>
|
||||||
|
<Step value="2" :disabled="disableLocationStep" :pt="stepPassthrough">
|
||||||
|
{{ $t('install.installLocation') }}
|
||||||
|
</Step>
|
||||||
|
<Step value="3" :disabled="disableSettingsStep" :pt="stepPassthrough">
|
||||||
|
{{ $t('install.desktopSettings') }}
|
||||||
|
</Step>
|
||||||
|
</StepList>
|
||||||
|
|
||||||
|
<!-- Next/Install button -->
|
||||||
|
<Button
|
||||||
|
:label="currentStep !== '3' ? $t('g.next') : $t('g.install')"
|
||||||
|
class="px-8 py-2 bg-brand-yellow hover:bg-brand-yellow/90 font-inter rounded-lg border-0 transition-colors justify-self-end"
|
||||||
|
:pt="{
|
||||||
|
label: { class: 'text-neutral-900 font-inter font-black' }
|
||||||
|
}"
|
||||||
|
:disabled="!canProceed"
|
||||||
|
@click="currentStep !== '3' ? $emit('next') : $emit('install')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PassThrough } from '@primevue/core'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Step, { type StepPassThroughOptions } from 'primevue/step'
|
||||||
|
import StepList from 'primevue/steplist'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
/** Current step index as string ('1', '2', '3', '4') */
|
||||||
|
currentStep: string
|
||||||
|
/** Whether the user can proceed to the next step */
|
||||||
|
canProceed: boolean
|
||||||
|
/** Whether the location step should be disabled */
|
||||||
|
disableLocationStep: boolean
|
||||||
|
/** Whether the migration step should be disabled */
|
||||||
|
disableMigrationStep: boolean
|
||||||
|
/** Whether the settings step should be disabled */
|
||||||
|
disableSettingsStep: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
previous: []
|
||||||
|
next: []
|
||||||
|
install: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const stepPassthrough: PassThrough<StepPassThroughOptions> = {
|
||||||
|
root: { class: 'flex-none p-0 m-0' },
|
||||||
|
header: ({ context }) => ({
|
||||||
|
class: [
|
||||||
|
'h-2.5 p-0 m-0 border-0 rounded-full transition-all duration-300',
|
||||||
|
context.active
|
||||||
|
? 'bg-brand-yellow w-8 rounded-sm'
|
||||||
|
: 'bg-neutral-700 w-2.5',
|
||||||
|
context.disabled ? 'opacity-60 cursor-not-allowed' : ''
|
||||||
|
].join(' ')
|
||||||
|
}),
|
||||||
|
number: { class: 'hidden' },
|
||||||
|
title: { class: 'hidden' }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
148
src/components/install/InstallLocationPicker.stories.ts
Normal file
148
src/components/install/InstallLocationPicker.stories.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// eslint-disable-next-line storybook/no-renderer-packages
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import InstallLocationPicker from './InstallLocationPicker.vue'
|
||||||
|
|
||||||
|
const meta: Meta<typeof InstallLocationPicker> = {
|
||||||
|
title: 'Desktop/Components/InstallLocationPicker',
|
||||||
|
component: InstallLocationPicker,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
backgrounds: {
|
||||||
|
default: 'dark',
|
||||||
|
values: [
|
||||||
|
{ name: 'dark', value: '#0a0a0a' },
|
||||||
|
{ name: 'neutral-900', value: '#171717' },
|
||||||
|
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => {
|
||||||
|
// Mock electron API
|
||||||
|
;(window as any).electronAPI = {
|
||||||
|
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 { template: '<story />' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Default story with accordion expanded
|
||||||
|
export const Default: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
components: { InstallLocationPicker },
|
||||||
|
setup() {
|
||||||
|
const installPath = ref('/Users/username/ComfyUI')
|
||||||
|
const pathError = ref('')
|
||||||
|
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
|
||||||
|
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
installPath,
|
||||||
|
pathError,
|
||||||
|
migrationSourcePath,
|
||||||
|
migrationItemIds
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="min-h-screen bg-neutral-950 p-8">
|
||||||
|
<InstallLocationPicker
|
||||||
|
v-model:installPath="installPath"
|
||||||
|
v-model:pathError="pathError"
|
||||||
|
v-model:migrationSourcePath="migrationSourcePath"
|
||||||
|
v-model:migrationItemIds="migrationItemIds"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Story with different background to test transparency
|
||||||
|
export const OnNeutral900: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
components: { InstallLocationPicker },
|
||||||
|
setup() {
|
||||||
|
const installPath = ref('/Users/username/ComfyUI')
|
||||||
|
const pathError = ref('')
|
||||||
|
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
|
||||||
|
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
installPath,
|
||||||
|
pathError,
|
||||||
|
migrationSourcePath,
|
||||||
|
migrationItemIds
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="min-h-screen bg-neutral-900 p-8">
|
||||||
|
<InstallLocationPicker
|
||||||
|
v-model:installPath="installPath"
|
||||||
|
v-model:pathError="pathError"
|
||||||
|
v-model:migrationSourcePath="migrationSourcePath"
|
||||||
|
v-model:migrationItemIds="migrationItemIds"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Story with debug overlay showing background colors
|
||||||
|
export const DebugBackgrounds: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
components: { InstallLocationPicker },
|
||||||
|
setup() {
|
||||||
|
const installPath = ref('/Users/username/ComfyUI')
|
||||||
|
const pathError = ref('')
|
||||||
|
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
|
||||||
|
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
installPath,
|
||||||
|
pathError,
|
||||||
|
migrationSourcePath,
|
||||||
|
migrationItemIds
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="min-h-screen bg-neutral-950 p-8 relative">
|
||||||
|
<div class="absolute top-4 right-4 text-white text-xs space-y-2 z-50">
|
||||||
|
<div>Parent bg: neutral-950 (#0a0a0a)</div>
|
||||||
|
<div>Accordion content: bg-transparent</div>
|
||||||
|
<div>Migration options: bg-transparent + p-4 rounded-lg</div>
|
||||||
|
</div>
|
||||||
|
<InstallLocationPicker
|
||||||
|
v-model:installPath="installPath"
|
||||||
|
v-model:pathError="pathError"
|
||||||
|
v-model:migrationSourcePath="migrationSourcePath"
|
||||||
|
v-model:migrationItemIds="migrationItemIds"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,103 +1,215 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-6 w-[600px]">
|
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
|
||||||
<!-- Installation Path Section -->
|
<!-- Installation Path Section -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="grow flex flex-col gap-6 text-neutral-300">
|
||||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||||
{{ $t('install.chooseInstallationLocation') }}
|
{{ $t('install.locationPicker.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="text-neutral-400 my-0">
|
<p class="text-center text-neutral-400 px-12">
|
||||||
{{ $t('install.installLocationDescription') }}
|
{{ $t('install.locationPicker.subtitle') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<!-- Path Input -->
|
||||||
<IconField class="flex-1">
|
<div class="flex gap-2 px-12">
|
||||||
<InputText
|
<InputText
|
||||||
v-model="installPath"
|
v-model="installPath"
|
||||||
class="w-full"
|
:placeholder="$t('install.locationPicker.pathPlaceholder')"
|
||||||
:class="{ 'p-invalid': pathError }"
|
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
|
||||||
@update:model-value="validatePath"
|
:class="{ 'p-invalid': pathError }"
|
||||||
@focus="onFocus"
|
@update:model-value="validatePath"
|
||||||
/>
|
@focus="onFocus"
|
||||||
<InputIcon
|
/>
|
||||||
v-tooltip.top="$t('install.installLocationTooltip')"
|
<Button
|
||||||
class="pi pi-info-circle"
|
icon="pi pi-folder-open"
|
||||||
/>
|
severity="secondary"
|
||||||
</IconField>
|
class="bg-neutral-700 hover:bg-neutral-600 border-0"
|
||||||
<Button icon="pi pi-folder" class="w-12" @click="browsePath" />
|
@click="browsePath"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Message v-if="pathError" severity="error" class="whitespace-pre-line">
|
<!-- Error Messages -->
|
||||||
{{ pathError }}
|
<div v-if="pathError || pathExists || nonDefaultDrive" class="px-12">
|
||||||
</Message>
|
<Message
|
||||||
<Message v-if="pathExists" severity="warn">
|
v-if="pathError"
|
||||||
{{ $t('install.pathExists') }}
|
severity="error"
|
||||||
</Message>
|
class="whitespace-pre-line w-full"
|
||||||
<Message v-if="nonDefaultDrive" severity="warn">
|
>
|
||||||
{{ $t('install.nonDefaultDrive') }}
|
{{ pathError }}
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
<Message v-if="pathExists" severity="warn" class="w-full">
|
||||||
|
{{ $t('install.pathExists') }}
|
||||||
<!-- System Paths Info -->
|
</Message>
|
||||||
<div class="bg-neutral-800 p-4 rounded-lg">
|
<Message v-if="nonDefaultDrive" severity="warn" class="w-full">
|
||||||
<h3 class="text-lg font-medium mt-0 mb-3 text-neutral-100">
|
{{ $t('install.nonDefaultDrive') }}
|
||||||
{{ $t('install.systemLocations') }}
|
</Message>
|
||||||
</h3>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i class="pi pi-folder text-neutral-400" />
|
|
||||||
<span class="text-neutral-400">App Data:</span>
|
|
||||||
<span class="text-neutral-200">{{ appData }}</span>
|
|
||||||
<span
|
|
||||||
v-tooltip="$t('install.appDataLocationTooltip')"
|
|
||||||
class="pi pi-info-circle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<i class="pi pi-desktop text-neutral-400" />
|
|
||||||
<span class="text-neutral-400">App Path:</span>
|
|
||||||
<span class="text-neutral-200">{{ appPath }}</span>
|
|
||||||
<span
|
|
||||||
v-tooltip="$t('install.appPathLocationTooltip')"
|
|
||||||
class="pi pi-info-circle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible Sections using PrimeVue Accordion -->
|
||||||
|
<Accordion
|
||||||
|
v-model:value="activeAccordionIndex"
|
||||||
|
:multiple="true"
|
||||||
|
class="location-picker-accordion"
|
||||||
|
:pt="{
|
||||||
|
root: 'bg-transparent border-0',
|
||||||
|
panel: {
|
||||||
|
root: 'border-0 mb-0'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
root: 'border-0',
|
||||||
|
content:
|
||||||
|
'text-neutral-400 hover:text-neutral-300 px-4 py-2 flex items-center gap-3',
|
||||||
|
toggleicon: 'text-xs order-first mr-0'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
root: 'bg-transparent border-0',
|
||||||
|
content: 'text-neutral-500 text-sm pl-11 pb-3 pt-0'
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<AccordionPanel value="0">
|
||||||
|
<AccordionHeader>
|
||||||
|
{{ $t('install.locationPicker.migrateFromExisting') }}
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionContent>
|
||||||
|
<MigrationPicker
|
||||||
|
v-model:source-path="migrationSourcePath"
|
||||||
|
v-model:migration-item-ids="migrationItemIds"
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPanel>
|
||||||
|
|
||||||
|
<AccordionPanel value="1">
|
||||||
|
<AccordionHeader>
|
||||||
|
{{ $t('install.locationPicker.chooseDownloadServers') }}
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionContent>
|
||||||
|
<template
|
||||||
|
v-for="([item, modelValue], index) in mirrors"
|
||||||
|
:key="item.settingId + item.mirror"
|
||||||
|
>
|
||||||
|
<Divider v-if="index > 0" class="my-8" />
|
||||||
|
|
||||||
|
<MirrorItem
|
||||||
|
v-model="modelValue.value"
|
||||||
|
:item="item"
|
||||||
|
@state-change="validationStates[index] = $event"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionPanel>
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||||
|
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
|
||||||
|
import Accordion from 'primevue/accordion'
|
||||||
|
import AccordionContent from 'primevue/accordioncontent'
|
||||||
|
import AccordionHeader from 'primevue/accordionheader'
|
||||||
|
import AccordionPanel from 'primevue/accordionpanel'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import IconField from 'primevue/iconfield'
|
import Divider from 'primevue/divider'
|
||||||
import InputIcon from 'primevue/inputicon'
|
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
import Message from 'primevue/message'
|
import Message from 'primevue/message'
|
||||||
import { onMounted, ref } from 'vue'
|
import { type ModelRef, computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||||
|
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||||
|
import {
|
||||||
|
PYPI_MIRROR,
|
||||||
|
PYTHON_MIRROR,
|
||||||
|
type UVMirror
|
||||||
|
} from '@/constants/uvMirrors'
|
||||||
import { electronAPI } from '@/utils/envUtil'
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
import { isInChina } from '@/utils/networkUtil'
|
||||||
|
import { ValidationState } from '@/utils/validationUtil'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const installPath = defineModel<string>('installPath', { required: true })
|
const installPath = defineModel<string>('installPath', { required: true })
|
||||||
const pathError = defineModel<string>('pathError', { required: true })
|
const pathError = defineModel<string>('pathError', { required: true })
|
||||||
|
const migrationSourcePath = defineModel<string>('migrationSourcePath')
|
||||||
|
const migrationItemIds = defineModel<string[]>('migrationItemIds')
|
||||||
|
const pythonMirror = defineModel<string>('pythonMirror', {
|
||||||
|
default: ''
|
||||||
|
})
|
||||||
|
const pypiMirror = defineModel<string>('pypiMirror', {
|
||||||
|
default: ''
|
||||||
|
})
|
||||||
|
const torchMirror = defineModel<string>('torchMirror', {
|
||||||
|
default: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const { device } = defineProps<{ device: TorchDeviceType | null }>()
|
||||||
|
|
||||||
const pathExists = ref(false)
|
const pathExists = ref(false)
|
||||||
const nonDefaultDrive = ref(false)
|
const nonDefaultDrive = ref(false)
|
||||||
const appData = ref('')
|
|
||||||
const appPath = ref('')
|
|
||||||
const inputTouched = ref(false)
|
const inputTouched = ref(false)
|
||||||
|
|
||||||
|
// Accordion state - array of active panel values
|
||||||
|
const activeAccordionIndex = ref<string[] | undefined>(undefined)
|
||||||
|
|
||||||
const electron = electronAPI()
|
const electron = electronAPI()
|
||||||
|
|
||||||
// Get system paths on component mount
|
// Mirror configuration logic
|
||||||
|
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||||
|
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||||
|
switch (device) {
|
||||||
|
case 'mps':
|
||||||
|
return {
|
||||||
|
settingId,
|
||||||
|
mirror: TorchMirrorUrl.NightlyCpu,
|
||||||
|
fallbackMirror: TorchMirrorUrl.NightlyCpu
|
||||||
|
}
|
||||||
|
case 'nvidia':
|
||||||
|
return {
|
||||||
|
settingId,
|
||||||
|
mirror: TorchMirrorUrl.Cuda,
|
||||||
|
fallbackMirror: TorchMirrorUrl.Cuda
|
||||||
|
}
|
||||||
|
case 'cpu':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
settingId,
|
||||||
|
mirror: PYPI_MIRROR.mirror,
|
||||||
|
fallbackMirror: PYPI_MIRROR.fallbackMirror
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIsInChina = ref(false)
|
||||||
|
const useFallbackMirror = (mirror: UVMirror) => ({
|
||||||
|
...mirror,
|
||||||
|
mirror: mirror.fallbackMirror
|
||||||
|
})
|
||||||
|
|
||||||
|
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
|
||||||
|
(
|
||||||
|
[
|
||||||
|
[PYTHON_MIRROR, pythonMirror],
|
||||||
|
[PYPI_MIRROR, pypiMirror],
|
||||||
|
[getTorchMirrorItem(device ?? 'cpu'), torchMirror]
|
||||||
|
] as [UVMirror, ModelRef<string>][]
|
||||||
|
).map(([item, modelValue]) => [
|
||||||
|
userIsInChina.value ? useFallbackMirror(item) : item,
|
||||||
|
modelValue
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const validationStates = ref<ValidationState[]>(
|
||||||
|
mirrors.value.map(() => ValidationState.IDLE)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get default install path on component mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const paths = await electron.getSystemPaths()
|
const paths = await electron.getSystemPaths()
|
||||||
appData.value = paths.appData
|
|
||||||
appPath.value = paths.appPath
|
|
||||||
installPath.value = paths.defaultInstallPath
|
installPath.value = paths.defaultInstallPath
|
||||||
|
|
||||||
await validatePath(paths.defaultInstallPath)
|
await validatePath(paths.defaultInstallPath)
|
||||||
|
userIsInChina.value = await isInChina()
|
||||||
})
|
})
|
||||||
|
|
||||||
const validatePath = async (path: string | undefined) => {
|
const validatePath = async (path: string | undefined) => {
|
||||||
@@ -151,3 +263,52 @@ const onFocus = async () => {
|
|||||||
await validatePath(installPath.value)
|
await validatePath(installPath.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference '../../assets/css/style.css';
|
||||||
|
|
||||||
|
:deep(.location-picker-accordion) {
|
||||||
|
@apply px-12;
|
||||||
|
|
||||||
|
.p-accordionpanel {
|
||||||
|
@apply border-0 bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-accordionheader {
|
||||||
|
@apply bg-neutral-800/50 border-0 rounded-xl mt-2 hover:bg-neutral-700/50;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-radius 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When panel is expanded, adjust header border radius */
|
||||||
|
.p-accordionpanel-active {
|
||||||
|
.p-accordionheader {
|
||||||
|
@apply rounded-t-xl rounded-b-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-accordioncontent {
|
||||||
|
@apply bg-neutral-800/50 border-0 rounded-b-xl rounded-t-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-accordioncontent-content {
|
||||||
|
@apply bg-transparent pt-3 pr-5 pb-5 pl-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override default chevron icons to use up/down */
|
||||||
|
.p-accordionheader-toggle-icon {
|
||||||
|
&::before {
|
||||||
|
content: '\e933';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-accordionpanel-active {
|
||||||
|
.p-accordionheader-toggle-icon {
|
||||||
|
&::before {
|
||||||
|
content: '\e902';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
45
src/components/install/MigrationPicker.stories.ts
Normal file
45
src/components/install/MigrationPicker.stories.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// eslint-disable-next-line storybook/no-renderer-packages
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import MigrationPicker from './MigrationPicker.vue'
|
||||||
|
|
||||||
|
const meta: Meta<typeof MigrationPicker> = {
|
||||||
|
title: 'Desktop/Components/MigrationPicker',
|
||||||
|
component: MigrationPicker,
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
default: 'dark',
|
||||||
|
values: [
|
||||||
|
{ name: 'dark', value: '#0a0a0a' },
|
||||||
|
{ name: 'neutral-900', value: '#171717' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => {
|
||||||
|
;(window as any).electronAPI = {
|
||||||
|
validateComfyUISource: () => Promise.resolve({ isValid: true }),
|
||||||
|
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { template: '<story />' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { MigrationPicker },
|
||||||
|
setup() {
|
||||||
|
const sourcePath = ref('')
|
||||||
|
const migrationItemIds = ref<string[]>([])
|
||||||
|
return { sourcePath, migrationItemIds }
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
'<MigrationPicker v-model:sourcePath="sourcePath" v-model:migrationItemIds="migrationItemIds" />'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
<div class="flex flex-col gap-6 w-[600px]">
|
<div class="flex flex-col gap-6 w-[600px]">
|
||||||
<!-- Source Location Section -->
|
<!-- Source Location Section -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
|
||||||
{{ $t('install.migrateFromExistingInstallation') }}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="text-neutral-400 my-0">
|
<p class="text-neutral-400 my-0">
|
||||||
{{ $t('install.migrationSourcePathDescription') }}
|
{{ $t('install.migrationSourcePathDescription') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -13,7 +9,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<InputText
|
<InputText
|
||||||
v-model="sourcePath"
|
v-model="sourcePath"
|
||||||
placeholder="Select existing ComfyUI installation (optional)"
|
:placeholder="$t('install.locationPicker.migrationPathPlaceholder')"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
:class="{ 'p-invalid': pathError }"
|
:class="{ 'p-invalid': pathError }"
|
||||||
@update:model-value="validateSource"
|
@update:model-value="validateSource"
|
||||||
@@ -27,10 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Migration Options -->
|
<!-- Migration Options -->
|
||||||
<div
|
<div v-if="isValidSource" class="flex flex-col gap-4 p-4 rounded-lg">
|
||||||
v-if="isValidSource"
|
|
||||||
class="flex flex-col gap-4 bg-neutral-800 p-4 rounded-lg"
|
|
||||||
>
|
|
||||||
<h3 class="text-lg mt-0 font-medium text-neutral-100">
|
<h3 class="text-lg mt-0 font-medium text-neutral-100">
|
||||||
{{ $t('install.selectItemsToMigrate') }}
|
{{ $t('install.selectItemsToMigrate') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Panel
|
|
||||||
:header="$t('install.settings.mirrorSettings')"
|
|
||||||
toggleable
|
|
||||||
:collapsed="!showMirrorInputs"
|
|
||||||
pt:root="bg-neutral-800 border-none w-[600px]"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="([item, modelValue], index) in mirrors"
|
|
||||||
:key="item.settingId + item.mirror"
|
|
||||||
>
|
|
||||||
<Divider v-if="index > 0" />
|
|
||||||
|
|
||||||
<MirrorItem
|
|
||||||
v-model="modelValue.value"
|
|
||||||
:item="item"
|
|
||||||
@state-change="validationStates[index] = $event"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #icons>
|
|
||||||
<i
|
|
||||||
v-tooltip="validationStateTooltip"
|
|
||||||
:class="{
|
|
||||||
'pi pi-spin pi-spinner text-neutral-400':
|
|
||||||
validationState === ValidationState.LOADING,
|
|
||||||
'pi pi-check text-green-500':
|
|
||||||
validationState === ValidationState.VALID,
|
|
||||||
'pi pi-times text-red-500':
|
|
||||||
validationState === ValidationState.INVALID
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Panel>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
|
||||||
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
|
|
||||||
import Divider from 'primevue/divider'
|
|
||||||
import Panel from 'primevue/panel'
|
|
||||||
import type { ModelRef } from 'vue'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
|
||||||
import type { UVMirror } from '@/constants/uvMirrors'
|
|
||||||
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/uvMirrors'
|
|
||||||
import { t } from '@/i18n'
|
|
||||||
import { isInChina } from '@/utils/networkUtil'
|
|
||||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
|
||||||
|
|
||||||
const showMirrorInputs = ref(false)
|
|
||||||
const { device } = defineProps<{ device: TorchDeviceType | null }>()
|
|
||||||
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
|
|
||||||
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
|
|
||||||
const torchMirror = defineModel<string>('torchMirror', { required: true })
|
|
||||||
|
|
||||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
|
||||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
|
||||||
switch (device) {
|
|
||||||
case 'mps':
|
|
||||||
return {
|
|
||||||
settingId,
|
|
||||||
mirror: TorchMirrorUrl.NightlyCpu,
|
|
||||||
fallbackMirror: TorchMirrorUrl.NightlyCpu
|
|
||||||
}
|
|
||||||
case 'nvidia':
|
|
||||||
return {
|
|
||||||
settingId,
|
|
||||||
mirror: TorchMirrorUrl.Cuda,
|
|
||||||
fallbackMirror: TorchMirrorUrl.Cuda
|
|
||||||
}
|
|
||||||
case 'cpu':
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
settingId,
|
|
||||||
mirror: PYPI_MIRROR.mirror,
|
|
||||||
fallbackMirror: PYPI_MIRROR.fallbackMirror
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userIsInChina = ref(false)
|
|
||||||
onMounted(async () => {
|
|
||||||
userIsInChina.value = await isInChina()
|
|
||||||
})
|
|
||||||
|
|
||||||
const useFallbackMirror = (mirror: UVMirror) => ({
|
|
||||||
...mirror,
|
|
||||||
mirror: mirror.fallbackMirror
|
|
||||||
})
|
|
||||||
|
|
||||||
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
|
|
||||||
(
|
|
||||||
[
|
|
||||||
[PYTHON_MIRROR, pythonMirror],
|
|
||||||
[PYPI_MIRROR, pypiMirror],
|
|
||||||
[getTorchMirrorItem(device ?? 'cpu'), torchMirror]
|
|
||||||
] as [UVMirror, ModelRef<string>][]
|
|
||||||
).map(([item, modelValue]) => [
|
|
||||||
userIsInChina.value ? useFallbackMirror(item) : item,
|
|
||||||
modelValue
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
const validationStates = ref<ValidationState[]>(
|
|
||||||
mirrors.value.map(() => ValidationState.IDLE)
|
|
||||||
)
|
|
||||||
const validationState = computed(() => {
|
|
||||||
return mergeValidationStates(validationStates.value)
|
|
||||||
})
|
|
||||||
const validationStateTooltip = computed(() => {
|
|
||||||
switch (validationState.value) {
|
|
||||||
case ValidationState.INVALID:
|
|
||||||
return t('install.settings.mirrorsUnreachable')
|
|
||||||
case ValidationState.VALID:
|
|
||||||
return t('install.settings.mirrorsReachable')
|
|
||||||
default:
|
|
||||||
return t('install.settings.checkingMirrors')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col gap-4 text-neutral-400 text-sm">
|
||||||
<div class="w-full">
|
<div>
|
||||||
<h3 class="text-lg font-medium text-neutral-100">
|
<h3 class="text-lg font-medium text-neutral-100 mb-3 mt-0">
|
||||||
{{ $t(`settings.${normalizedSettingId}.name`) }}
|
{{ $t(`settings.${normalizedSettingId}.name`) }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-neutral-400 mt-1">
|
<p class="my-1">
|
||||||
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
|
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,18 +16,61 @@
|
|||||||
"
|
"
|
||||||
@state-change="validationState = $event"
|
@state-change="validationState = $event"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="secondParagraph" class="mt-2">
|
||||||
|
<a href="#" @click.prevent="showDialog = true">
|
||||||
|
{{ $t('g.learnMore') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="showDialog"
|
||||||
|
modal
|
||||||
|
dismissable-mask
|
||||||
|
:header="$t(`settings.${normalizedSettingId}.urlFormatTitle`)"
|
||||||
|
class="select-none max-w-3xl"
|
||||||
|
>
|
||||||
|
<div class="text-neutral-300">
|
||||||
|
<p class="mt-1 whitespace-pre-wrap">{{ secondParagraph }}</p>
|
||||||
|
<div class="mt-2 break-all">
|
||||||
|
<span class="text-neutral-300 font-semibold">
|
||||||
|
{{ EXAMPLE_URL_FIRST_PART }}
|
||||||
|
</span>
|
||||||
|
<span>{{ EXAMPLE_URL_SECOND_PART }}</span>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<p>
|
||||||
|
{{ $t(`settings.${normalizedSettingId}.fileUrlDescription`) }}
|
||||||
|
</p>
|
||||||
|
<span class="text-neutral-300 font-semibold">
|
||||||
|
{{ FILE_URL_SCHEME }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ EXAMPLE_FILE_URL }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Dialog from 'primevue/dialog'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import UrlInput from '@/components/common/UrlInput.vue'
|
import UrlInput from '@/components/common/UrlInput.vue'
|
||||||
import type { UVMirror } from '@/constants/uvMirrors'
|
import type { UVMirror } from '@/constants/uvMirrors'
|
||||||
|
import { st } from '@/i18n'
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||||
import { ValidationState } from '@/utils/validationUtil'
|
import { ValidationState } from '@/utils/validationUtil'
|
||||||
|
|
||||||
|
const FILE_URL_SCHEME = 'file://'
|
||||||
|
const EXAMPLE_FILE_URL = '/C:/MyPythonInstallers/'
|
||||||
|
const EXAMPLE_URL_FIRST_PART =
|
||||||
|
'https://github.com/astral-sh/python-build-standalone/releases/download'
|
||||||
|
const EXAMPLE_URL_SECOND_PART =
|
||||||
|
'/20250902/cpython-3.12.11+20250902-x86_64-pc-windows-msvc-install_only.tar.gz'
|
||||||
|
|
||||||
const { item } = defineProps<{
|
const { item } = defineProps<{
|
||||||
item: UVMirror
|
item: UVMirror
|
||||||
}>()
|
}>()
|
||||||
@@ -38,11 +81,16 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const modelValue = defineModel<string>('modelValue', { required: true })
|
const modelValue = defineModel<string>('modelValue', { required: true })
|
||||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||||
|
const showDialog = ref(false)
|
||||||
|
|
||||||
const normalizedSettingId = computed(() => {
|
const normalizedSettingId = computed(() => {
|
||||||
return normalizeI18nKey(item.settingId)
|
return normalizeI18nKey(item.settingId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const secondParagraph = computed(() =>
|
||||||
|
st(`settings.${normalizedSettingId.value}.urlDescription`, '')
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
modelValue.value = item.mirror
|
modelValue.value = item.mirror
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
|||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon()
|
||||||
const terminal = markRaw(
|
const terminal = markRaw(
|
||||||
new Terminal({
|
new Terminal({
|
||||||
convertEol: true
|
convertEol: true,
|
||||||
|
theme: { background: '#171717' }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
terminal.loadAddon(fitAddon)
|
terminal.loadAddon(fitAddon)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"calculatingDimensions": "Calculating dimensions",
|
"calculatingDimensions": "Calculating dimensions",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"loadAllFolders": "Load All Folders",
|
"loadAllFolders": "Load All Folders",
|
||||||
|
"logoAlt": "ComfyUI Logo",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"refreshNode": "Refresh Node",
|
"refreshNode": "Refresh Node",
|
||||||
"terminal": "Terminal",
|
"terminal": "Terminal",
|
||||||
@@ -406,6 +407,27 @@
|
|||||||
"migration": "Migration",
|
"migration": "Migration",
|
||||||
"desktopSettings": "Desktop Settings",
|
"desktopSettings": "Desktop Settings",
|
||||||
"chooseInstallationLocation": "Choose Installation Location",
|
"chooseInstallationLocation": "Choose Installation Location",
|
||||||
|
"gpuPicker": {
|
||||||
|
"title": "Choose your hardware setup",
|
||||||
|
"recommended": "RECOMMENDED",
|
||||||
|
"nvidiaSubtitle": "NVIDIA CUDA",
|
||||||
|
"cpuSubtitle": "CPU Mode",
|
||||||
|
"manualSubtitle": "Manual Setup",
|
||||||
|
"appleMetalDescription": "Leverages your Mac's GPU for faster speed and a better overall experience",
|
||||||
|
"nvidiaDescription": "Use your NVIDIA GPU with CUDA acceleration for the best performance.",
|
||||||
|
"cpuDescription": "Use CPU mode for compatibility when GPU acceleration is not available",
|
||||||
|
"manualDescription": "Configure ComfyUI manually for advanced setups or unsupported hardware"
|
||||||
|
},
|
||||||
|
"locationPicker": {
|
||||||
|
"title": "Choose where to install ComfyUI",
|
||||||
|
"subtitle": "Pick a folder for ComfyUI's files. We'll also set up Python there automatically.",
|
||||||
|
"pathPlaceholder": "/Users/username/Documents/ComfyUI",
|
||||||
|
"migrationPathPlaceholder": "Select existing ComfyUI installation (optional)",
|
||||||
|
"migrateFromExisting": "Migrate from existing installation",
|
||||||
|
"migrateDescription": "Copy or link your existing models, custom nodes, and configurations from a previous ComfyUI installation.",
|
||||||
|
"chooseDownloadServers": "Choose download servers manually",
|
||||||
|
"downloadServersDescription": "Select specific mirror servers for downloading Python, PyPI packages, and PyTorch based on your location."
|
||||||
|
},
|
||||||
"systemLocations": "System Locations",
|
"systemLocations": "System Locations",
|
||||||
"failedToSelectDirectory": "Failed to select directory",
|
"failedToSelectDirectory": "Failed to select directory",
|
||||||
"pathValidationFailed": "Failed to validate path",
|
"pathValidationFailed": "Failed to validate path",
|
||||||
@@ -490,18 +512,26 @@
|
|||||||
"metricsDisabled": "Metrics Disabled",
|
"metricsDisabled": "Metrics Disabled",
|
||||||
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
|
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
|
||||||
},
|
},
|
||||||
|
"desktopStart": {
|
||||||
|
"initialising": "Initialising..."
|
||||||
|
},
|
||||||
"serverStart": {
|
"serverStart": {
|
||||||
|
"title": "Starting ComfyUI",
|
||||||
"troubleshoot": "Troubleshoot",
|
"troubleshoot": "Troubleshoot",
|
||||||
"reportIssue": "Report Issue",
|
"reportIssue": "Report Issue",
|
||||||
"openLogs": "Open Logs",
|
"openLogs": "Open Logs",
|
||||||
"showTerminal": "Show Terminal",
|
"showTerminal": "Show Terminal",
|
||||||
"copySelectionTooltip": "Copy selection",
|
"copySelectionTooltip": "Copy selection",
|
||||||
"copyAllTooltip": "Copy all",
|
"copyAllTooltip": "Copy all",
|
||||||
|
"errorMessage": "Unable to start ComfyUI Desktop",
|
||||||
|
"installation": {
|
||||||
|
"title": "Installing ComfyUI"
|
||||||
|
},
|
||||||
"process": {
|
"process": {
|
||||||
"initial-state": "Loading...",
|
"initial-state": "Loading...",
|
||||||
"python-setup": "Setting up Python Environment...",
|
"python-setup": "Setting up Python Environment...",
|
||||||
"starting-server": "Starting ComfyUI server...",
|
"starting-server": "Starting ComfyUI server...",
|
||||||
"ready": "Finishing...",
|
"ready": "Loading Human Interface",
|
||||||
"error": "Unable to start ComfyUI Desktop"
|
"error": "Unable to start ComfyUI Desktop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
"name": "Send anonymous usage metrics"
|
"name": "Send anonymous usage metrics"
|
||||||
},
|
},
|
||||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||||
"name": "Pypi Install Mirror",
|
"name": "PyPI Install Mirror",
|
||||||
"tooltip": "Default pip install mirror"
|
"tooltip": "Default pip install mirror"
|
||||||
},
|
},
|
||||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||||
"name": "Python Install Mirror",
|
"name": "Python Install Mirror",
|
||||||
"tooltip": "Managed Python installations are downloaded from the Astral python-build-standalone project. This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Distributions can be read from a local directory by using the file:// URL scheme."
|
"tooltip": "Managed Python installations are downloaded from the Astral python-build-standalone project. To use a different source for Python installations, enter a mirror URL.",
|
||||||
|
"urlFormatTitle": "Mirror URL Format",
|
||||||
|
"urlDescription": "This is an example python download URL.\n\nThe mirror URL is the first half, including everything before the date (20250902):",
|
||||||
|
"fileUrlDescription": "To install from a file you downloaded earlier, you may use a file URL:"
|
||||||
},
|
},
|
||||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||||
"name": "Torch Install Mirror",
|
"name": "Torch Install Mirror",
|
||||||
@@ -421,4 +424,4 @@
|
|||||||
"pysssss_SnapToGrid": {
|
"pysssss_SnapToGrid": {
|
||||||
"name": "Always snap to grid"
|
"name": "Always snap to grid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,3 @@ export enum ValidationState {
|
|||||||
VALID = 'VALID',
|
VALID = 'VALID',
|
||||||
INVALID = 'INVALID'
|
INVALID = 'INVALID'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mergeValidationStates = (states: ValidationState[]) => {
|
|
||||||
if (states.some((state) => state === ValidationState.INVALID)) {
|
|
||||||
return ValidationState.INVALID
|
|
||||||
}
|
|
||||||
if (states.some((state) => state === ValidationState.LOADING)) {
|
|
||||||
return ValidationState.LOADING
|
|
||||||
}
|
|
||||||
if (states.every((state) => state === ValidationState.VALID)) {
|
|
||||||
return ValidationState.VALID
|
|
||||||
}
|
|
||||||
return ValidationState.IDLE
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
<StartupDisplay :title="$t('desktopStart.initialising')" />
|
||||||
</BaseViewTemplate>
|
</BaseViewTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import StartupDisplay from '@/components/common/StartupDisplay.vue'
|
||||||
|
|
||||||
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
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>
|
<template>
|
||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<!-- h-full to make sure the stepper does not layout shift between steps
|
<!-- Fixed height container with flexbox layout for proper content management -->
|
||||||
as for each step the stepper height is different. Inherit the center element
|
<div class="w-full h-full flex flex-col">
|
||||||
placement from BaseViewTemplate would cause layout shift. -->
|
<Stepper
|
||||||
<Stepper
|
v-model:value="currentStep"
|
||||||
class="h-full p-8 2xl:p-16"
|
class="flex flex-col h-full"
|
||||||
value="0"
|
@update:value="handleStepChange"
|
||||||
@update:value="handleStepChange"
|
>
|
||||||
>
|
<!-- Main content area that grows to fill available space -->
|
||||||
<StepList class="select-none">
|
<StepPanels
|
||||||
<Step value="0">
|
class="flex-1 overflow-auto"
|
||||||
{{ $t('install.gpu') }}
|
:style="{ scrollbarGutter: 'stable' }"
|
||||||
</Step>
|
>
|
||||||
<Step value="1" :disabled="noGpu">
|
<StepPanel value="1" class="flex">
|
||||||
{{ $t('install.installLocation') }}
|
<GpuPicker v-model:device="device" />
|
||||||
</Step>
|
</StepPanel>
|
||||||
<Step value="2" :disabled="noGpu || hasError || highestStep < 1">
|
<StepPanel value="2">
|
||||||
{{ $t('install.migration') }}
|
<InstallLocationPicker
|
||||||
</Step>
|
v-model:install-path="installPath"
|
||||||
<Step value="3" :disabled="noGpu || hasError || highestStep < 2">
|
v-model:path-error="pathError"
|
||||||
{{ $t('install.desktopSettings') }}
|
v-model:migration-source-path="migrationSourcePath"
|
||||||
</Step>
|
v-model:migration-item-ids="migrationItemIds"
|
||||||
</StepList>
|
v-model:python-mirror="pythonMirror"
|
||||||
<StepPanels>
|
v-model:pypi-mirror="pypiMirror"
|
||||||
<StepPanel v-slot="{ activateCallback }" value="0">
|
v-model:torch-mirror="torchMirror"
|
||||||
<GpuPicker v-model:device="device" />
|
: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')"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</StepPanel>
|
||||||
</StepPanel>
|
<StepPanel value="3">
|
||||||
<StepPanel v-slot="{ activateCallback }" value="1">
|
<DesktopSettingsConfiguration
|
||||||
<InstallLocationPicker
|
v-model:auto-update="autoUpdate"
|
||||||
v-model:install-path="installPath"
|
v-model:allow-metrics="allowMetrics"
|
||||||
v-model:path-error="pathError"
|
|
||||||
/>
|
|
||||||
<div class="flex pt-6 justify-between">
|
|
||||||
<Button
|
|
||||||
:label="$t('g.back')"
|
|
||||||
severity="secondary"
|
|
||||||
icon="pi pi-arrow-left"
|
|
||||||
@click="activateCallback('0')"
|
|
||||||
/>
|
/>
|
||||||
<Button
|
</StepPanel>
|
||||||
:label="$t('g.next')"
|
</StepPanels>
|
||||||
icon="pi pi-arrow-right"
|
|
||||||
icon-pos="right"
|
<!-- Install footer with navigation -->
|
||||||
:disabled="pathError !== ''"
|
<InstallFooter
|
||||||
@click="activateCallback('2')"
|
class="w-full max-w-2xl my-6 mx-auto"
|
||||||
/>
|
:current-step
|
||||||
</div>
|
:can-proceed
|
||||||
</StepPanel>
|
:disable-location-step="noGpu"
|
||||||
<StepPanel v-slot="{ activateCallback }" value="2">
|
:disable-migration-step="noGpu || hasError || highestStep < 2"
|
||||||
<MigrationPicker
|
:disable-settings-step="noGpu || hasError || highestStep < 3"
|
||||||
v-model:source-path="migrationSourcePath"
|
@previous="goToPreviousStep"
|
||||||
v-model:migration-item-ids="migrationItemIds"
|
@next="goToNextStep"
|
||||||
/>
|
@install="install"
|
||||||
<div class="flex pt-6 justify-between">
|
/>
|
||||||
<Button
|
</Stepper>
|
||||||
:label="$t('g.back')"
|
</div>
|
||||||
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:auto-update="autoUpdate"
|
|
||||||
v-model:allow-metrics="allowMetrics"
|
|
||||||
/>
|
|
||||||
<MirrorsConfiguration
|
|
||||||
v-model:python-mirror="pythonMirror"
|
|
||||||
v-model:pypi-mirror="pypiMirror"
|
|
||||||
v-model:torch-mirror="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>
|
|
||||||
</BaseViewTemplate>
|
</BaseViewTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -114,9 +57,6 @@ import type {
|
|||||||
InstallOptions,
|
InstallOptions,
|
||||||
TorchDeviceType
|
TorchDeviceType
|
||||||
} from '@comfyorg/comfyui-electron-types'
|
} 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 StepPanel from 'primevue/steppanel'
|
||||||
import StepPanels from 'primevue/steppanels'
|
import StepPanels from 'primevue/steppanels'
|
||||||
import Stepper from 'primevue/stepper'
|
import Stepper from 'primevue/stepper'
|
||||||
@@ -125,9 +65,8 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
|
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
|
||||||
import GpuPicker from '@/components/install/GpuPicker.vue'
|
import GpuPicker from '@/components/install/GpuPicker.vue'
|
||||||
|
import InstallFooter from '@/components/install/InstallFooter.vue'
|
||||||
import InstallLocationPicker from '@/components/install/InstallLocationPicker.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 { electronAPI } from '@/utils/envUtil'
|
||||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||||
|
|
||||||
@@ -145,6 +84,9 @@ const pythonMirror = ref('')
|
|||||||
const pypiMirror = ref('')
|
const pypiMirror = ref('')
|
||||||
const torchMirror = ref('')
|
const torchMirror = ref('')
|
||||||
|
|
||||||
|
/** Current step in the stepper */
|
||||||
|
const currentStep = ref('1')
|
||||||
|
|
||||||
/** Forces each install step to be visited at least once. */
|
/** Forces each install step to be visited at least once. */
|
||||||
const highestStep = ref(0)
|
const highestStep = ref(0)
|
||||||
|
|
||||||
@@ -164,6 +106,40 @@ const setHighestStep = (value: string | number) => {
|
|||||||
const hasError = computed(() => pathError.value !== '')
|
const hasError = computed(() => pathError.value !== '')
|
||||||
const noGpu = computed(() => typeof device.value !== 'string')
|
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 electron = electronAPI()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const install = async () => {
|
const install = async () => {
|
||||||
@@ -195,7 +171,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||||
step: '0',
|
step: currentStep.value,
|
||||||
gpu: detectedGpu
|
gpu: detectedGpu
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -205,6 +181,30 @@ onMounted(async () => {
|
|||||||
@reference '../assets/css/style.css';
|
@reference '../assets/css/style.css';
|
||||||
|
|
||||||
:deep(.p-steppanel) {
|
: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;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
|
||||||
|
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,76 +1,205 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark class="flex-col">
|
<BaseViewTemplate dark>
|
||||||
<div class="flex flex-col w-full h-full items-center">
|
<div class="relative min-h-screen">
|
||||||
<h2 class="text-2xl font-bold">
|
<!-- Terminal Background Layer (always visible during loading) -->
|
||||||
{{ t(`serverStart.process.${status}`) }}
|
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
|
||||||
<span v-if="status === ProgressStatus.ERROR">
|
<div class="h-full w-full">
|
||||||
v{{ electronVersion }}
|
<BaseTerminal @created="terminalCreated" />
|
||||||
</span>
|
</div>
|
||||||
</h2>
|
</div>
|
||||||
<div
|
|
||||||
v-if="status === ProgressStatus.ERROR"
|
<!-- Semi-transparent overlay -->
|
||||||
class="flex flex-col items-center gap-4"
|
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
|
||||||
>
|
|
||||||
<div class="flex items-center my-4 gap-2">
|
<!-- Smooth radial gradient overlay -->
|
||||||
<Button
|
<div
|
||||||
icon="pi pi-flag"
|
v-if="!isError"
|
||||||
severity="secondary"
|
class="fixed inset-0 z-8"
|
||||||
:label="t('serverStart.reportIssue')"
|
style="
|
||||||
@click="reportIssue"
|
background: radial-gradient(
|
||||||
/>
|
ellipse 800px 600px at center,
|
||||||
<Button
|
rgba(23, 23, 23, 0.95) 0%,
|
||||||
icon="pi pi-file"
|
rgba(23, 23, 23, 0.93) 10%,
|
||||||
severity="secondary"
|
rgba(23, 23, 23, 0.9) 20%,
|
||||||
:label="t('serverStart.openLogs')"
|
rgba(23, 23, 23, 0.85) 30%,
|
||||||
@click="openLogs"
|
rgba(23, 23, 23, 0.75) 40%,
|
||||||
/>
|
rgba(23, 23, 23, 0.6) 50%,
|
||||||
<Button
|
rgba(23, 23, 23, 0.4) 60%,
|
||||||
icon="pi pi-wrench"
|
rgba(23, 23, 23, 0.2) 70%,
|
||||||
:label="t('serverStart.troubleshoot')"
|
rgba(23, 23, 23, 0.1) 80%,
|
||||||
@click="troubleshoot"
|
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>
|
</div>
|
||||||
<Button
|
|
||||||
v-if="!terminalVisible"
|
|
||||||
icon="pi pi-search"
|
|
||||||
severity="secondary"
|
|
||||||
:label="t('serverStart.showTerminal')"
|
|
||||||
@click="terminalVisible = true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
|
|
||||||
</div>
|
</div>
|
||||||
</BaseViewTemplate>
|
</BaseViewTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ProgressStatus } from '@comfyorg/comfyui-electron-types'
|
import {
|
||||||
|
InstallStage,
|
||||||
|
type InstallStageInfo,
|
||||||
|
type InstallStageName,
|
||||||
|
ProgressStatus
|
||||||
|
} from '@comfyorg/comfyui-electron-types'
|
||||||
import type { Terminal } from '@xterm/xterm'
|
import type { Terminal } from '@xterm/xterm'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
||||||
|
import StartupDisplay from '@/components/common/StartupDisplay.vue'
|
||||||
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
|
||||||
import { electronAPI } from '@/utils/envUtil'
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||||
|
|
||||||
const electron = electronAPI()
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const electron = electronAPI()
|
||||||
|
|
||||||
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||||
const electronVersion = ref<string>('')
|
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
|
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 }) => {
|
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||||
status.value = newStatus
|
status.value = newStatus
|
||||||
|
|
||||||
// Make critical error screen more obvious.
|
// Make critical error screen more obvious.
|
||||||
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
|
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
|
||||||
else xterm?.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalCreated = (
|
const terminalCreated = (
|
||||||
@@ -95,9 +224,30 @@ const reportIssue = () => {
|
|||||||
}
|
}
|
||||||
const openLogs = () => electron.openLogsFolder()
|
const openLogs = () => electron.openLogsFolder()
|
||||||
|
|
||||||
|
let cleanupInstallStageListener: (() => void) | undefined
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
electron.sendReady()
|
electron.sendReady()
|
||||||
electron.onProgressUpdate(updateProgress)
|
electron.onProgressUpdate(updateProgress)
|
||||||
|
cleanupInstallStageListener =
|
||||||
|
electron.InstallStage.onUpdate(updateInstallStage)
|
||||||
|
|
||||||
|
const stageInfo = await electron.InstallStage.getCurrent()
|
||||||
|
updateInstallStage(stageInfo)
|
||||||
electronVersion.value = await electron.getElectronVersion()
|
electronVersion.value = await electron.getElectronVersion()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
xterm?.dispose()
|
||||||
|
cleanupInstallStageListener?.()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference '../assets/css/style.css';
|
||||||
|
|
||||||
|
/* Hide the xterm scrollbar completely */
|
||||||
|
:deep(.p-terminal) .xterm-viewport {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
<!-- Header -->
|
<div class="grid grid-rows-2 gap-8">
|
||||||
<h1 class="animated-gradient-text text-glow select-none">
|
<!-- Top container: Logo -->
|
||||||
{{ $t('welcome.title') }}
|
<div class="flex items-end justify-center">
|
||||||
</h1>
|
<img
|
||||||
|
src="/assets/images/comfy-brand-mark.svg"
|
||||||
<!-- Get Started Button -->
|
:alt="$t('g.logoAlt')"
|
||||||
<Button
|
class="w-60"
|
||||||
:label="$t('welcome.getStarted')"
|
/>
|
||||||
icon="pi pi-arrow-right"
|
</div>
|
||||||
icon-pos="right"
|
<!-- Bottom container: Title and button -->
|
||||||
size="large"
|
<div class="flex flex-col items-center justify-center gap-4">
|
||||||
rounded
|
<Button
|
||||||
class="p-4 text-lg fade-in-up"
|
:label="$t('welcome.getStarted')"
|
||||||
@click="navigateTo('/install')"
|
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>
|
</div>
|
||||||
</BaseViewTemplate>
|
</BaseViewTemplate>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,49 +37,3 @@ const navigateTo = async (path: string) => {
|
|||||||
await router.push(path)
|
await router.push(path)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference '../assets/css/style.css';
|
|
||||||
|
|
||||||
.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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user