mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 01:20:09 +00:00
Migrate settings dialog to Vue (#335)
* Basic setting dialog * Add custom setting value render * handle combo options * Add input slider * 100% width for select dropdown
This commit is contained in:
116
src/components/dialog/content/SettingDialogContent.vue
Normal file
116
src/components/dialog/content/SettingDialogContent.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<table class="comfy-modal-content comfy-table">
|
||||
<tbody>
|
||||
<tr v-for="setting in sortedSettings" :key="setting.id">
|
||||
<td>
|
||||
<span>
|
||||
{{ setting.name }}
|
||||
</span>
|
||||
<Chip
|
||||
v-if="setting.tooltip"
|
||||
icon="pi pi-info-circle"
|
||||
severity="secondary"
|
||||
v-tooltip="setting.tooltip"
|
||||
class="info-chip"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<component
|
||||
:is="markRaw(getSettingComponent(setting))"
|
||||
:id="setting.id"
|
||||
:modelValue="settingStore.get(setting.id)"
|
||||
@update:modelValue="updateSetting(setting, $event)"
|
||||
v-bind="getSettingAttrs(setting)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Component, computed, markRaw } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Select from 'primevue/select'
|
||||
import Chip from 'primevue/chip'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
|
||||
import InputSlider from '@/components/dialog/content/setting/InputSlider.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const sortedSettings = computed<SettingParams[]>(() => {
|
||||
return Object.values(settingStore.settings)
|
||||
.filter((setting: SettingParams) => setting.type !== 'hidden')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
function getSettingAttrs(setting: SettingParams) {
|
||||
const attrs = { ...(setting.attrs || {}) }
|
||||
const settingType = setting.type
|
||||
if (typeof settingType === 'function') {
|
||||
attrs['renderFunction'] = () =>
|
||||
settingType(
|
||||
setting.name,
|
||||
(v) => updateSetting(setting, v),
|
||||
settingStore.get(setting.id),
|
||||
setting.attrs
|
||||
)
|
||||
}
|
||||
switch (setting.type) {
|
||||
case 'combo':
|
||||
attrs['options'] = setting.options
|
||||
if (typeof setting.options[0] !== 'string') {
|
||||
attrs['optionLabel'] = 'text'
|
||||
attrs['optionValue'] = 'value'
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
attrs['class'] += ' comfy-vue-setting-input'
|
||||
return attrs
|
||||
}
|
||||
|
||||
function getSettingComponent(setting: SettingParams): Component {
|
||||
if (typeof setting.type === 'function') {
|
||||
// return setting.type(
|
||||
// setting.name, (v) => updateSetting(setting, v), settingStore.get(setting.id), setting.attrs)
|
||||
return CustomSettingValue
|
||||
}
|
||||
switch (setting.type) {
|
||||
case 'boolean':
|
||||
return ToggleSwitch
|
||||
case 'number':
|
||||
return InputNumber
|
||||
case 'slider':
|
||||
return InputSlider
|
||||
case 'combo':
|
||||
return Select
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
}
|
||||
|
||||
const updateSetting = (setting: SettingParams, value: any) => {
|
||||
if (setting.onChange) setting.onChange(value, settingStore.get(setting.id))
|
||||
|
||||
settingStore.set(setting.id, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.info-chip {
|
||||
background: transparent !important;
|
||||
}
|
||||
.comfy-vue-setting-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.comfy-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
25
src/components/dialog/content/setting/CustomSettingValue.vue
Normal file
25
src/components/dialog/content/setting/CustomSettingValue.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div ref="container"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
renderFunction: () => HTMLElement
|
||||
}>()
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
|
||||
function renderContent() {
|
||||
if (container.value) {
|
||||
container.value.innerHTML = ''
|
||||
const element = props.renderFunction()
|
||||
container.value.appendChild(element)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(renderContent)
|
||||
|
||||
watch(() => props.renderFunction, renderContent)
|
||||
</script>
|
||||
58
src/components/dialog/content/setting/InputSlider.vue
Normal file
58
src/components/dialog/content/setting/InputSlider.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="input-slider">
|
||||
<Slider
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="updateValue"
|
||||
class="slider-part"
|
||||
:class="sliderClass"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<InputText
|
||||
:value="modelValue"
|
||||
@input="updateValue"
|
||||
class="input-part"
|
||||
:class="inputClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Slider from 'primevue/slider'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number
|
||||
inputClass?: string
|
||||
sliderClass?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
const updateValue = (newValue: string | number) => {
|
||||
const numValue =
|
||||
typeof newValue === 'string' ? parseFloat(newValue) : newValue
|
||||
if (!isNaN(numValue)) {
|
||||
emit('update:modelValue', numValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
/* Adjust this value to control space between slider and input */
|
||||
}
|
||||
|
||||
.slider-part {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.input-part {
|
||||
width: 5rem;
|
||||
/* Adjust this value to control input width */
|
||||
}
|
||||
</style>
|
||||
@@ -7,10 +7,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { app } from '@/scripts/app'
|
||||
import SideBarIcon from './SideBarIcon.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__']
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const showSetting = () => {
|
||||
app.ui.settings.show()
|
||||
dialogStore.showDialog({
|
||||
title: `Settings (v${frontendVersion})`,
|
||||
component: SettingDialogContent
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -185,6 +185,9 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
}
|
||||
|
||||
this.settingsParamLookup[id] = params
|
||||
if (this.app.vueAppReady) {
|
||||
useSettingStore().settings[id] = params
|
||||
}
|
||||
this.settingsLookup[id] = {
|
||||
id,
|
||||
onChange,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { Component } from 'vue'
|
||||
import { type Component, markRaw } from 'vue'
|
||||
|
||||
interface DialogState {
|
||||
isVisible: boolean
|
||||
@@ -26,7 +26,7 @@ export const useDialogStore = defineStore('dialog', {
|
||||
props?: Record<string, any>
|
||||
}) {
|
||||
this.title = options.title
|
||||
this.component = options.component
|
||||
this.component = markRaw(options.component)
|
||||
this.props = options.props || {}
|
||||
this.isVisible = true
|
||||
},
|
||||
|
||||
@@ -9,15 +9,18 @@
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfySettingsDialog } from '@/scripts/ui/settings'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface State {
|
||||
settingValues: Record<string, any>
|
||||
settings: Record<string, SettingParams>
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore('setting', {
|
||||
state: (): State => ({
|
||||
settingValues: {}
|
||||
settingValues: {},
|
||||
settings: {}
|
||||
}),
|
||||
actions: {
|
||||
addSettings(settings: ComfySettingsDialog) {
|
||||
@@ -25,6 +28,7 @@ export const useSettingStore = defineStore('setting', {
|
||||
const value = settings.getSettingValue(id)
|
||||
this.settingValues[id] = value
|
||||
}
|
||||
this.settings = settings.settingsParamLookup
|
||||
},
|
||||
|
||||
set(key: string, value: any) {
|
||||
|
||||
Reference in New Issue
Block a user