mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
[Electron] ComfyUI Desktop install wizard (#1503)
* Basic prototype * Welcome screen animation * nit * Refactor structure * Fix mocking * Add tooltips * i18n * Add next button * nit * Stepper navigate * Extract * More i18n * More i18n * Polish MigrationPicker * Polish settings step * Add more i18n * nit * nit
This commit is contained in:
112
src/components/install/DesktopSettingsConfiguration.vue
Normal file
112
src/components/install/DesktopSettingsConfiguration.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6 w-[600px]">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<h2 class="text-2xl font-semibold text-neutral-100">
|
||||||
|
{{ $t('install.desktopAppSettings') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-neutral-400 my-0">
|
||||||
|
{{ $t('install.desktopAppSettingsDescription') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg">
|
||||||
|
<!-- Auto Update Setting -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-lg font-medium text-neutral-100">
|
||||||
|
{{ $t('install.settings.autoUpdate') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-neutral-400 mt-1">
|
||||||
|
{{ $t('install.settings.autoUpdateDescription') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<InputSwitch v-model="autoUpdate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<!-- Metrics Collection Setting -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-lg font-medium text-neutral-100">
|
||||||
|
{{ $t('install.settings.allowMetrics') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-neutral-400 mt-1">
|
||||||
|
{{ $t('install.settings.allowMetricsDescription') }}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm text-blue-400 hover:text-blue-300 mt-1 inline-block"
|
||||||
|
@click.prevent="showMetricsInfo"
|
||||||
|
>
|
||||||
|
{{ $t('install.settings.learnMoreAboutData') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<InputSwitch v-model="allowMetrics" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Dialog -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="showDialog"
|
||||||
|
modal
|
||||||
|
:header="$t('install.settings.dataCollectionDialog.title')"
|
||||||
|
>
|
||||||
|
<div class="text-neutral-300">
|
||||||
|
<h4 class="font-medium mb-2">
|
||||||
|
{{ $t('install.settings.dataCollectionDialog.whatWeCollect') }}
|
||||||
|
</h4>
|
||||||
|
<ul class="list-disc pl-6 space-y-1">
|
||||||
|
<li>
|
||||||
|
{{ $t('install.settings.dataCollectionDialog.errorReports') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ $t('install.settings.dataCollectionDialog.systemInfo') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 class="font-medium mt-4 mb-2">
|
||||||
|
{{ $t('install.settings.dataCollectionDialog.whatWeDoNotCollect') }}
|
||||||
|
</h4>
|
||||||
|
<ul class="list-disc pl-6 space-y-1">
|
||||||
|
<li>
|
||||||
|
{{
|
||||||
|
$t('install.settings.dataCollectionDialog.personalInformation')
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ $t('install.settings.dataCollectionDialog.workflowContents') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{
|
||||||
|
$t('install.settings.dataCollectionDialog.fileSystemInformation')
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'install.settings.dataCollectionDialog.customNodeConfigurations'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import InputSwitch from 'primevue/inputswitch'
|
||||||
|
import Dialog from 'primevue/dialog'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
|
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const autoUpdate = defineModel('autoUpdate', { required: true })
|
||||||
|
const allowMetrics = defineModel('allowMetrics', { required: true })
|
||||||
|
|
||||||
|
const showMetricsInfo = () => {
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
115
src/components/install/InstallLocationPicker.vue
Normal file
115
src/components/install/InstallLocationPicker.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6 w-[600px]">
|
||||||
|
<!-- Installation Path Section -->
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<h2 class="text-2xl font-semibold text-neutral-100">
|
||||||
|
{{ $t('install.chooseInstallationLocation') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-neutral-400 my-0">
|
||||||
|
{{ $t('install.installLocationDescription') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<IconField class="flex-1">
|
||||||
|
<InputText
|
||||||
|
v-model="installPath"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': pathError }"
|
||||||
|
@change="validatePath"
|
||||||
|
/>
|
||||||
|
<InputIcon
|
||||||
|
class="pi pi-info-circle"
|
||||||
|
v-tooltip="$t('install.installLocationTooltip')"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
<Button icon="pi pi-folder" @click="browsePath" class="w-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Message v-if="pathError" severity="error">
|
||||||
|
{{ pathError }}
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Paths Info -->
|
||||||
|
<div class="bg-neutral-800 p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-medium mt-0 mb-3 text-neutral-100">
|
||||||
|
{{ $t('install.systemLocations') }}
|
||||||
|
</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
|
||||||
|
class="pi pi-info-circle"
|
||||||
|
v-tooltip="$t('install.appDataLocationTooltip')"
|
||||||
|
></span>
|
||||||
|
</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
|
||||||
|
class="pi pi-info-circle"
|
||||||
|
v-tooltip="$t('install.appPathLocationTooltip')"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import IconField from 'primevue/iconfield'
|
||||||
|
import InputIcon from 'primevue/inputicon'
|
||||||
|
import Message from 'primevue/message'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const installPath = defineModel<string>('installPath', { required: true })
|
||||||
|
const pathError = defineModel<string>('pathError', { required: true })
|
||||||
|
const appData = ref('')
|
||||||
|
const appPath = ref('')
|
||||||
|
|
||||||
|
// TODO: Implement the actual electron API.
|
||||||
|
const electron = electronAPI() as any
|
||||||
|
|
||||||
|
// Get system paths on component mount
|
||||||
|
onMounted(async () => {
|
||||||
|
const paths = await electron.getSystemPaths()
|
||||||
|
appData.value = paths.appData
|
||||||
|
appPath.value = paths.appPath
|
||||||
|
installPath.value = paths.defaultInstallPath
|
||||||
|
})
|
||||||
|
|
||||||
|
const validatePath = async () => {
|
||||||
|
try {
|
||||||
|
pathError.value = ''
|
||||||
|
const validation = await electron.validateInstallPath(installPath.value)
|
||||||
|
|
||||||
|
if (!validation.isValid) {
|
||||||
|
pathError.value = validation.error
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
pathError.value = t('install.pathValidationFailed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const browsePath = async () => {
|
||||||
|
try {
|
||||||
|
const result = await electron.showDirectoryPicker()
|
||||||
|
if (result) {
|
||||||
|
installPath.value = result
|
||||||
|
await validatePath()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
pathError.value = t('install.failedToSelectDirectory')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
136
src/components/install/MigrationPicker.vue
Normal file
136
src/components/install/MigrationPicker.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6 w-[600px]">
|
||||||
|
<!-- Source Location Section -->
|
||||||
|
<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">
|
||||||
|
{{ $t('install.migrationSourcePathDescription') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<InputText
|
||||||
|
v-model="sourcePath"
|
||||||
|
placeholder="Select existing ComfyUI installation (optional)"
|
||||||
|
class="flex-1"
|
||||||
|
:class="{ 'p-invalid': pathError }"
|
||||||
|
@change="validateSource"
|
||||||
|
/>
|
||||||
|
<Button icon="pi pi-folder" @click="browsePath" class="w-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Message v-if="pathError" severity="error">
|
||||||
|
{{ pathError }}
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Migration Options -->
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
{{ $t('install.selectItemsToMigrate') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
v-for="item in migrationItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex items-center gap-3 p-2 hover:bg-neutral-700 rounded"
|
||||||
|
@click="item.selected = !item.selected"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-model="item.selected"
|
||||||
|
:inputId="item.id"
|
||||||
|
:binary="true"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label :for="item.id" class="text-neutral-200 font-medium">
|
||||||
|
{{ item.label }}
|
||||||
|
</label>
|
||||||
|
<p class="text-sm text-neutral-400 my-1">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skip Migration -->
|
||||||
|
<div v-else class="text-neutral-400 italic">
|
||||||
|
{{ $t('install.migrationOptional') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watchEffect } from 'vue'
|
||||||
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Checkbox from 'primevue/checkbox'
|
||||||
|
import Message from 'primevue/message'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const electron = electronAPI() as any
|
||||||
|
|
||||||
|
const sourcePath = defineModel<string>('sourcePath', { required: false })
|
||||||
|
const migrationItemIds = defineModel<string[]>('migrationItemIds', {
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const migrationItems = ref([])
|
||||||
|
const pathError = ref('')
|
||||||
|
const isValidSource = computed(
|
||||||
|
() => sourcePath.value !== '' && pathError.value === ''
|
||||||
|
)
|
||||||
|
|
||||||
|
const validateSource = async () => {
|
||||||
|
if (!sourcePath.value) {
|
||||||
|
pathError.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pathError.value = ''
|
||||||
|
const validation = await electron.validateComfyUISource(sourcePath.value)
|
||||||
|
|
||||||
|
if (!validation.isValid) pathError.value = validation.error
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
pathError.value = t('install.pathValidationFailed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const browsePath = async () => {
|
||||||
|
try {
|
||||||
|
const result = await electron.showDirectoryPicker()
|
||||||
|
if (result) {
|
||||||
|
sourcePath.value = result
|
||||||
|
await validateSource()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
pathError.value = t('install.failedToSelectDirectory')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
migrationItems.value = (await electron.migrationItems()).map((item) => ({
|
||||||
|
...item,
|
||||||
|
selected: true
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
migrationItemIds.value = migrationItems.value
|
||||||
|
.filter((item) => item.selected)
|
||||||
|
.map((item) => item.id)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
|
||||||
<!-- Header -->
|
|
||||||
<h1 class="text-4xl font-bold text-neutral-100">Welcome to ComfyUI</h1>
|
|
||||||
|
|
||||||
<!-- Get Started Button -->
|
|
||||||
<Button
|
|
||||||
label="Get Started"
|
|
||||||
icon="pi pi-arrow-right"
|
|
||||||
iconPos="right"
|
|
||||||
size="large"
|
|
||||||
@click="$emit('start')"
|
|
||||||
class="p-4 text-lg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Installation Steps -->
|
|
||||||
<div class="w-[600px]">
|
|
||||||
<Steps :model="installSteps" :readonly="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Steps from 'primevue/steps'
|
|
||||||
import Button from 'primevue/button'
|
|
||||||
|
|
||||||
const installSteps = [
|
|
||||||
{
|
|
||||||
label: 'Install Location',
|
|
||||||
icon: 'pi pi-folder'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Migration',
|
|
||||||
icon: 'pi pi-download'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Desktop Settings',
|
|
||||||
icon: 'pi pi-desktop'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Review',
|
|
||||||
icon: 'pi pi-check'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
defineEmits(['start'])
|
|
||||||
</script>
|
|
||||||
45
src/i18n.ts
45
src/i18n.ts
@@ -2,6 +2,51 @@ import { createI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
en: {
|
en: {
|
||||||
|
install: {
|
||||||
|
installLocation: 'Install Location',
|
||||||
|
migration: 'Migration',
|
||||||
|
desktopSettings: 'Desktop Settings',
|
||||||
|
chooseInstallationLocation: 'Choose Installation Location',
|
||||||
|
systemLocations: 'System Locations',
|
||||||
|
failedToSelectDirectory: 'Failed to select directory',
|
||||||
|
pathValidationFailed: 'Failed to validate path',
|
||||||
|
installLocationDescription:
|
||||||
|
"Select the directory for ComfyUI's user data. A python environment will be installed to the selected location. Please make sure the selected disk has enough space (~5GB) left.",
|
||||||
|
installLocationTooltip:
|
||||||
|
"ComfyUI's user data directory. Stores:\n- Python Environment\n- Models\n- Custom nodes\n",
|
||||||
|
appDataLocationTooltip:
|
||||||
|
"ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
|
||||||
|
appPathLocationTooltip:
|
||||||
|
"ComfyUI's app asset directory. Stores the ComfyUI code and assets",
|
||||||
|
migrateFromExistingInstallation: 'Migrate from Existing Installation',
|
||||||
|
migrationSourcePathDescription:
|
||||||
|
'If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation.',
|
||||||
|
selectItemsToMigrate: 'Select Items to Migrate',
|
||||||
|
migrationOptional:
|
||||||
|
"Migration is optional. If you don't have an existing installation, you can skip this step.",
|
||||||
|
desktopAppSettings: 'Desktop App Settings',
|
||||||
|
desktopAppSettingsDescription:
|
||||||
|
'Configure how ComfyUI behaves on your desktop. You can change these settings later.',
|
||||||
|
settings: {
|
||||||
|
autoUpdate: 'Automatic Updates',
|
||||||
|
allowMetrics: 'Usage Analytics',
|
||||||
|
autoUpdateDescription:
|
||||||
|
"Automatically download and install updates when they become available. You'll always be notified before updates are installed.",
|
||||||
|
allowMetricsDescription:
|
||||||
|
'Help improve ComfyUI by sending anonymous usage data. No personal information or workflow content will be collected.',
|
||||||
|
learnMoreAboutData: 'Learn more about data collection',
|
||||||
|
dataCollectionDialog: {
|
||||||
|
title: 'About Data Collection',
|
||||||
|
whatWeCollect: 'What we collect:',
|
||||||
|
whatWeDoNotCollect: "What we don't collect:",
|
||||||
|
errorReports: 'Error reports',
|
||||||
|
systemInfo: 'Operating system and app version',
|
||||||
|
personalInformation: 'Personal information',
|
||||||
|
workflowContent: 'Workflow content',
|
||||||
|
fileSystemInformation: 'File system information'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
loadAllFolders: 'Load All Folders',
|
loadAllFolders: 'Load All Folders',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import {
|
import {
|
||||||
createRouter,
|
createRouter,
|
||||||
createWebHashHistory,
|
createWebHashHistory,
|
||||||
createWebHistory
|
createWebHistory,
|
||||||
|
NavigationGuardNext,
|
||||||
|
RouteLocationNormalized
|
||||||
} from 'vue-router'
|
} from 'vue-router'
|
||||||
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
||||||
import { isElectron } from './utils/envUtil'
|
import { isElectron } from './utils/envUtil'
|
||||||
|
|
||||||
const isFileProtocol = () => window.location.protocol === 'file:'
|
const isFileProtocol = () => window.location.protocol === 'file:'
|
||||||
|
const guardElectronAccess = (
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
from: RouteLocationNormalized,
|
||||||
|
next: NavigationGuardNext
|
||||||
|
) => {
|
||||||
|
if (isElectron()) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: isFileProtocol() ? createWebHashHistory() : createWebHistory(),
|
history: isFileProtocol() ? createWebHashHistory() : createWebHistory(),
|
||||||
@@ -24,27 +37,19 @@ const router = createRouter({
|
|||||||
path: 'server-start',
|
path: 'server-start',
|
||||||
name: 'ServerStartView',
|
name: 'ServerStartView',
|
||||||
component: () => import('@/views/ServerStartView.vue'),
|
component: () => import('@/views/ServerStartView.vue'),
|
||||||
beforeEnter: async (to, from, next) => {
|
beforeEnter: guardElectronAccess
|
||||||
// Only allow access to this page in electron environment
|
|
||||||
if (isElectron()) {
|
|
||||||
next()
|
|
||||||
} else {
|
|
||||||
next('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'install',
|
path: 'install',
|
||||||
name: 'InstallView',
|
name: 'InstallView',
|
||||||
component: () => import('@/views/InstallView.vue'),
|
component: () => import('@/views/InstallView.vue'),
|
||||||
beforeEnter: async (to, from, next) => {
|
beforeEnter: guardElectronAccess
|
||||||
// Only allow access to this page in electron environment
|
},
|
||||||
if (isElectron()) {
|
{
|
||||||
next()
|
path: 'welcome',
|
||||||
} else {
|
name: 'WelcomeView',
|
||||||
next('/')
|
component: () => import('@/views/WelcomeView.vue'),
|
||||||
}
|
beforeEnter: guardElectronAccess
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="font-sans flex flex-col justify-center items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
class="font-sans flex flex-col items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||||
>
|
>
|
||||||
<WelcomeScreen />
|
<Stepper class="mt-[5vh] 2xl:mt-[20vh]" value="1">
|
||||||
|
<StepList>
|
||||||
|
<Step value="1" :disabled="hasError">
|
||||||
|
{{ $t('install.installLocation') }}
|
||||||
|
</Step>
|
||||||
|
<Step value="2" :disabled="hasError">
|
||||||
|
{{ $t('install.migration') }}
|
||||||
|
</Step>
|
||||||
|
<Step value="3" :disabled="hasError">
|
||||||
|
{{ $t('install.desktopSettings') }}
|
||||||
|
</Step>
|
||||||
|
</StepList>
|
||||||
|
<StepPanels>
|
||||||
|
<StepPanel value="1" v-slot="{ activateCallback }">
|
||||||
|
<InstallLocationPicker
|
||||||
|
v-model:installPath="installPath"
|
||||||
|
v-model:pathError="pathError"
|
||||||
|
/>
|
||||||
|
<div class="flex pt-6 justify-end">
|
||||||
|
<Button
|
||||||
|
label="Next"
|
||||||
|
icon="pi pi-arrow-right"
|
||||||
|
iconPos="right"
|
||||||
|
@click="activateCallback('2')"
|
||||||
|
:disabled="pathError !== ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StepPanel>
|
||||||
|
<StepPanel value="2" v-slot="{ activateCallback }">
|
||||||
|
<MigrationPicker
|
||||||
|
v-model:sourcePath="migrationSourcePath"
|
||||||
|
v-model:migrationItemIds="migrationItemIds"
|
||||||
|
/>
|
||||||
|
<div class="flex pt-6 justify-between">
|
||||||
|
<Button
|
||||||
|
label="Back"
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-arrow-left"
|
||||||
|
@click="activateCallback('1')"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Next"
|
||||||
|
icon="pi pi-arrow-right"
|
||||||
|
iconPos="right"
|
||||||
|
@click="activateCallback('3')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StepPanel>
|
||||||
|
<StepPanel value="3" v-slot="{ activateCallback }">
|
||||||
|
<DesktopSettingsConfiguration
|
||||||
|
v-model:autoUpdate="autoUpdate"
|
||||||
|
v-model:allowMetrics="allowMetrics"
|
||||||
|
/>
|
||||||
|
<div class="flex pt-6 justify-between">
|
||||||
|
<Button
|
||||||
|
label="Back"
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-arrow-left"
|
||||||
|
@click="activateCallback('2')"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Install"
|
||||||
|
icon="pi pi-check"
|
||||||
|
iconPos="right"
|
||||||
|
@click="install()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StepPanel>
|
||||||
|
</StepPanels>
|
||||||
|
</Stepper>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import WelcomeScreen from '@/components/install/WelcomeScreen.vue'
|
import Button from 'primevue/button'
|
||||||
|
import Stepper from 'primevue/stepper'
|
||||||
|
import StepList from 'primevue/steplist'
|
||||||
|
import StepPanels from 'primevue/steppanels'
|
||||||
|
import Step from 'primevue/step'
|
||||||
|
import StepPanel from 'primevue/steppanel'
|
||||||
|
|
||||||
|
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
|
||||||
|
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||||
|
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
|
||||||
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const installPath = ref('')
|
||||||
|
const pathError = ref('')
|
||||||
|
|
||||||
|
const migrationSourcePath = ref('')
|
||||||
|
const migrationItemIds = ref<string[]>([])
|
||||||
|
|
||||||
|
const autoUpdate = ref(true)
|
||||||
|
const allowMetrics = ref(true)
|
||||||
|
|
||||||
|
const hasError = computed(() => pathError.value !== '')
|
||||||
|
|
||||||
|
const install = () => {
|
||||||
|
;(electronAPI() as any).installComfyUI({
|
||||||
|
installPath: installPath.value,
|
||||||
|
autoUpdate: autoUpdate.value,
|
||||||
|
allowMetrics: allowMetrics.value,
|
||||||
|
migrationSourcePath: migrationSourcePath.value,
|
||||||
|
migrationItemIds: migrationItemIds.value
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.p-steppanel) {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
69
src/views/WelcomeView.vue
Normal file
69
src/views/WelcomeView.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="font-sans flex flex-col justify-center items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<h1 class="animated-gradient-text text-glow">Welcome to ComfyUI</h1>
|
||||||
|
|
||||||
|
<!-- Get Started Button -->
|
||||||
|
<Button
|
||||||
|
label="Get Started"
|
||||||
|
icon="pi pi-arrow-right"
|
||||||
|
iconPos="right"
|
||||||
|
size="large"
|
||||||
|
rounded
|
||||||
|
@click="$router.push('/install')"
|
||||||
|
class="p-4 text-lg fade-in-up"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.animated-gradient-text {
|
||||||
|
@apply font-bold;
|
||||||
|
font-size: clamp(2rem, 8vw, 4rem);
|
||||||
|
background: linear-gradient(to right, #12c2e9, #c471ed, #f64f59, #12c2e9);
|
||||||
|
background-size: 300% auto;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: gradient 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow {
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% center;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 300% center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-up {
|
||||||
|
animation: fadeInUp 1.5s ease-out;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,16 +2,6 @@ import { defineConfig, Plugin } from 'vite'
|
|||||||
import { mergeConfig } from 'vite'
|
import { mergeConfig } from 'vite'
|
||||||
import type { UserConfig } from 'vitest/config'
|
import type { UserConfig } from 'vitest/config'
|
||||||
import baseConfig from './vite.config.mts'
|
import baseConfig from './vite.config.mts'
|
||||||
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
|
||||||
|
|
||||||
const electronAPIMock: Partial<ElectronAPI> = {
|
|
||||||
sendReady: () => {},
|
|
||||||
onShowSelectDirectory: () => {},
|
|
||||||
onFirstTimeSetupComplete: () => {},
|
|
||||||
onProgressUpdate: () => {},
|
|
||||||
onLogMessage: () => {},
|
|
||||||
isFirstTimeSetup: () => Promise.resolve(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockElectronAPI: Plugin = {
|
const mockElectronAPI: Plugin = {
|
||||||
name: 'mock-electron-api',
|
name: 'mock-electron-api',
|
||||||
@@ -19,7 +9,41 @@ const mockElectronAPI: Plugin = {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: 'script',
|
tag: 'script',
|
||||||
children: `window.electronAPI = ${JSON.stringify(electronAPIMock)};`
|
children: `window.electronAPI = {
|
||||||
|
sendReady: () => {},
|
||||||
|
onShowSelectDirectory: () => {},
|
||||||
|
onFirstTimeSetupComplete: () => {},
|
||||||
|
onProgressUpdate: () => {},
|
||||||
|
onLogMessage: () => {},
|
||||||
|
isFirstTimeSetup: () => Promise.resolve(true),
|
||||||
|
getSystemPaths: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
appData: 'C:/Users/username/AppData/Roaming',
|
||||||
|
appPath: 'C:/Program Files/comfyui-electron/resources/app',
|
||||||
|
defaultInstallPath: 'C:/Users/username/comfyui-electron'
|
||||||
|
}),
|
||||||
|
validateInstallPath: (path) => {
|
||||||
|
if (path === 'bad') {
|
||||||
|
return { isValid: false, error: 'Bad path!' }
|
||||||
|
}
|
||||||
|
return { isValid: true }
|
||||||
|
},
|
||||||
|
migrationItems: () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
id: 'user_files',
|
||||||
|
label: 'User Files',
|
||||||
|
description: 'Settings and user-created workflows'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
validateComfyUISource: (path) => {
|
||||||
|
if (path === 'bad') {
|
||||||
|
return { isValid: false, error: 'Bad path!' }
|
||||||
|
}
|
||||||
|
return { isValid: true }
|
||||||
|
},
|
||||||
|
showDirectoryPicker: () => Promise.resolve('C:/Users/username/comfyui-electron/1')
|
||||||
|
};`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user