mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 15:29:44 +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>
|
||||
Reference in New Issue
Block a user