mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Categorize setting items (#338)
* Basic setting panel rework * refactor * Style the setting item * Reject invalid value * nit * nit * Sort settings by label * info chip as icon * nit
This commit is contained in:
@@ -12,7 +12,11 @@
|
|||||||
@unmaximize="maximized = false"
|
@unmaximize="maximized = false"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3>{{ dialogStore.title || ' ' }}</h3>
|
<component
|
||||||
|
v-if="dialogStore.headerComponent"
|
||||||
|
:is="dialogStore.headerComponent"
|
||||||
|
/>
|
||||||
|
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<component
|
<component
|
||||||
|
|||||||
@@ -1,116 +1,119 @@
|
|||||||
<template>
|
<template>
|
||||||
<table class="comfy-modal-content comfy-table">
|
<div class="settings-container">
|
||||||
<tbody>
|
<div class="settings-sidebar">
|
||||||
<tr v-for="setting in sortedSettings" :key="setting.id">
|
<Listbox
|
||||||
<td>
|
v-model="activeCategory"
|
||||||
<span>
|
:options="categories"
|
||||||
{{ setting.name }}
|
optionLabel="label"
|
||||||
</span>
|
scrollHeight="100%"
|
||||||
<Chip
|
:pt="{ root: { class: 'border-none' } }"
|
||||||
v-if="setting.tooltip"
|
/>
|
||||||
icon="pi pi-info-circle"
|
</div>
|
||||||
severity="secondary"
|
<Divider layout="vertical" />
|
||||||
v-tooltip="setting.tooltip"
|
<div class="settings-content" v-if="activeCategory">
|
||||||
class="info-chip"
|
<Tabs :value="activeCategory.label">
|
||||||
/>
|
<TabPanels>
|
||||||
</td>
|
<TabPanel
|
||||||
<td>
|
v-for="category in categories"
|
||||||
<component
|
:key="category.key"
|
||||||
:is="markRaw(getSettingComponent(setting))"
|
:value="category.label"
|
||||||
:id="setting.id"
|
>
|
||||||
:modelValue="settingStore.get(setting.id)"
|
<SettingGroup
|
||||||
@update:modelValue="updateSetting(setting, $event)"
|
v-for="group in sortedGroups(category)"
|
||||||
v-bind="getSettingAttrs(setting)"
|
:key="group.label"
|
||||||
/>
|
:group="{
|
||||||
</td>
|
label: group.label,
|
||||||
</tr>
|
settings: flattenTree<SettingParams>(group)
|
||||||
</tbody>
|
}"
|
||||||
</table>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Component, computed, markRaw } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import InputText from 'primevue/inputtext'
|
import Listbox from 'primevue/listbox'
|
||||||
import InputNumber from 'primevue/inputnumber'
|
import Tabs from 'primevue/tabs'
|
||||||
import Select from 'primevue/select'
|
import TabPanels from 'primevue/tabpanels'
|
||||||
import Chip from 'primevue/chip'
|
import TabPanel from 'primevue/tabpanel'
|
||||||
import ToggleSwitch from 'primevue/toggleswitch'
|
import Divider from 'primevue/divider'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||||
import { SettingParams } from '@/types/settingTypes'
|
import { SettingParams } from '@/types/settingTypes'
|
||||||
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
|
import SettingGroup from './setting/SettingGroup.vue'
|
||||||
import InputSlider from '@/components/dialog/content/setting/InputSlider.vue'
|
import { flattenTree } from '@/utils/treeUtil'
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const sortedSettings = computed<SettingParams[]>(() => {
|
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
||||||
return Object.values(settingStore.settings)
|
const categories = computed<SettingTreeNode[]>(
|
||||||
.filter((setting: SettingParams) => setting.type !== 'hidden')
|
() => settingRoot.value.children || []
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
)
|
||||||
|
|
||||||
|
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||||
|
|
||||||
|
watch(activeCategory, (newCategory, oldCategory) => {
|
||||||
|
if (newCategory === null) {
|
||||||
|
activeCategory.value = oldCategory
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getSettingAttrs(setting: SettingParams) {
|
onMounted(() => {
|
||||||
const attrs = { ...(setting.attrs || {}) }
|
activeCategory.value = categories.value[0]
|
||||||
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'
|
const sortedGroups = (category: SettingTreeNode) => {
|
||||||
return attrs
|
return [...(category.children || [])].sort((a, b) =>
|
||||||
}
|
a.label.localeCompare(b.label)
|
||||||
|
)
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.info-chip {
|
/* Remove after we have tailwind setup */
|
||||||
background: transparent !important;
|
.border-none {
|
||||||
}
|
border: none !important;
|
||||||
.comfy-vue-setting-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.comfy-table {
|
.settings-container {
|
||||||
|
display: flex;
|
||||||
|
height: 80vh;
|
||||||
|
width: 60vw;
|
||||||
|
max-width: 1000px;
|
||||||
|
overflow: hidden;
|
||||||
|
/* Prevents container from scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* Prevents sidebar from shrinking */
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Allows vertical scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the Listbox takes full width of the sidebar */
|
||||||
|
.settings-sidebar :deep(.p-listbox) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Optional: Style scrollbars for webkit browsers */
|
||||||
|
.settings-sidebar::-webkit-scrollbar,
|
||||||
|
.settings-content::-webkit-scrollbar {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar::-webkit-scrollbar-thumb,
|
||||||
|
.settings-content::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,37 +5,68 @@
|
|||||||
@update:modelValue="updateValue"
|
@update:modelValue="updateValue"
|
||||||
class="slider-part"
|
class="slider-part"
|
||||||
:class="sliderClass"
|
:class="sliderClass"
|
||||||
v-bind="$attrs"
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
/>
|
/>
|
||||||
<InputText
|
<InputNumber
|
||||||
:value="modelValue"
|
:modelValue="modelValue"
|
||||||
@input="updateValue"
|
@update:modelValue="updateValue"
|
||||||
class="input-part"
|
class="input-part"
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputText from 'primevue/inputtext'
|
import { ref, watch } from 'vue'
|
||||||
|
import InputNumber from 'primevue/inputnumber'
|
||||||
import Slider from 'primevue/slider'
|
import Slider from 'primevue/slider'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: number
|
modelValue: number
|
||||||
inputClass?: string
|
inputClass?: string
|
||||||
sliderClass?: string
|
sliderClass?: string
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
step?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: number): void
|
(e: 'update:modelValue', value: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const updateValue = (newValue: string | number) => {
|
const localValue = ref(props.modelValue)
|
||||||
const numValue =
|
|
||||||
typeof newValue === 'string' ? parseFloat(newValue) : newValue
|
watch(
|
||||||
if (!isNaN(numValue)) {
|
() => props.modelValue,
|
||||||
emit('update:modelValue', numValue)
|
(newValue) => {
|
||||||
|
localValue.value = newValue
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateValue = (newValue: number | null) => {
|
||||||
|
if (newValue === null) {
|
||||||
|
// If the input is cleared, reset to the minimum value or 0
|
||||||
|
newValue = Number(props.min) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = Number(props.min) || Number.NEGATIVE_INFINITY
|
||||||
|
const max = Number(props.max) || Number.POSITIVE_INFINITY
|
||||||
|
const step = Number(props.step) || 1
|
||||||
|
|
||||||
|
// Ensure the value is within the allowed range
|
||||||
|
newValue = Math.max(min, Math.min(max, newValue))
|
||||||
|
|
||||||
|
// Round to the nearest step
|
||||||
|
newValue = Math.round(newValue / step) * step
|
||||||
|
|
||||||
|
// Update local value and emit change
|
||||||
|
localValue.value = newValue
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -44,7 +75,6 @@ const updateValue = (newValue: string | number) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
/* Adjust this value to control space between slider and input */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-part {
|
.slider-part {
|
||||||
@@ -52,7 +82,6 @@ const updateValue = (newValue: string | number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-part {
|
.input-part {
|
||||||
width: 5rem;
|
width: 5rem !important;
|
||||||
/* Adjust this value to control input width */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
143
src/components/dialog/content/setting/SettingGroup.vue
Normal file
143
src/components/dialog/content/setting/SettingGroup.vue
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="setting-group">
|
||||||
|
<Divider />
|
||||||
|
<h3>{{ group.label }}</h3>
|
||||||
|
<div
|
||||||
|
v-for="setting in group.settings"
|
||||||
|
:key="setting.id"
|
||||||
|
class="setting-item"
|
||||||
|
>
|
||||||
|
<div class="setting-label">
|
||||||
|
<span
|
||||||
|
>{{ setting.name }}
|
||||||
|
<i
|
||||||
|
v-if="setting.tooltip"
|
||||||
|
class="pi pi-info-circle info-chip"
|
||||||
|
v-tooltip="setting.tooltip"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-input">
|
||||||
|
<component
|
||||||
|
:is="markRaw(getSettingComponent(setting))"
|
||||||
|
:id="setting.id"
|
||||||
|
:modelValue="settingStore.get(setting.id)"
|
||||||
|
@update:modelValue="updateSetting(setting, $event)"
|
||||||
|
v-bind="getSettingAttrs(setting)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
import { SettingParams } from '@/types/settingTypes'
|
||||||
|
import { markRaw, type Component } from 'vue'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import InputNumber from 'primevue/inputnumber'
|
||||||
|
import Select from 'primevue/select'
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
|
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
|
||||||
|
import InputSlider from '@/components/dialog/content/setting/InputSlider.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
group: {
|
||||||
|
label: string
|
||||||
|
settings: SettingParams[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSetting = (setting: SettingParams, value: any) => {
|
||||||
|
if (setting.onChange) setting.onChange(value, settingStore.get(setting.id))
|
||||||
|
settingStore.set(setting.id, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSettingComponent(setting: SettingParams): Component {
|
||||||
|
if (typeof setting.type === 'function') {
|
||||||
|
return CustomSettingValue
|
||||||
|
}
|
||||||
|
switch (setting.type) {
|
||||||
|
case 'boolean':
|
||||||
|
return ToggleSwitch
|
||||||
|
case 'number':
|
||||||
|
return InputNumber
|
||||||
|
case 'slider':
|
||||||
|
return InputSlider
|
||||||
|
case 'combo':
|
||||||
|
return Select
|
||||||
|
default:
|
||||||
|
return InputText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.info-chip {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-input {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure PrimeVue components take full width of their container */
|
||||||
|
.setting-input :deep(.p-inputtext),
|
||||||
|
.setting-input :deep(.input-slider),
|
||||||
|
.setting-input :deep(.p-select),
|
||||||
|
.setting-input :deep(.p-togglebutton) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-input :deep(.p-inputtext) {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special case for ToggleSwitch to align it to the right */
|
||||||
|
.setting-input :deep(.p-toggleswitch) {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/components/dialog/header/SettingDialogHeader.vue
Normal file
24
src/components/dialog/header/SettingDialogHeader.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<i class="pi pi-cog"></i>
|
||||||
|
<span>Settings</span>
|
||||||
|
<Tag :value="frontendVersion" severity="secondary" class="version-tag" />
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Tag from 'primevue/tag'
|
||||||
|
const frontendVersion = 'v' + window['__COMFYUI_FRONTEND_VERSION__']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pi-cog {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.version-tag {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
import SideBarIcon from './SideBarIcon.vue'
|
import SideBarIcon from './SideBarIcon.vue'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||||
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__']
|
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||||
|
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
const showSetting = () => {
|
const showSetting = () => {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
title: `Settings (v${frontendVersion})`,
|
headerComponent: SettingDialogHeader,
|
||||||
component: SettingDialogContent
|
component: SettingDialogContent
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
import { $el } from '../ui'
|
import { $el } from '../ui'
|
||||||
|
|
||||||
export class ComfyDialog<
|
export class ComfyDialog<
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { type Component, markRaw } from 'vue'
|
|||||||
interface DialogState {
|
interface DialogState {
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
title: string
|
title: string
|
||||||
|
headerComponent: Component | null
|
||||||
component: Component | null
|
component: Component | null
|
||||||
props: Record<string, any>
|
props: Record<string, any>
|
||||||
}
|
}
|
||||||
@@ -15,6 +16,7 @@ export const useDialogStore = defineStore('dialog', {
|
|||||||
state: (): DialogState => ({
|
state: (): DialogState => ({
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
title: '',
|
title: '',
|
||||||
|
headerComponent: null,
|
||||||
component: null,
|
component: null,
|
||||||
props: {}
|
props: {}
|
||||||
}),
|
}),
|
||||||
@@ -22,10 +24,12 @@ export const useDialogStore = defineStore('dialog', {
|
|||||||
actions: {
|
actions: {
|
||||||
showDialog(options: {
|
showDialog(options: {
|
||||||
title?: string
|
title?: string
|
||||||
|
headerComponent?: Component
|
||||||
component: Component
|
component: Component
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
}) {
|
}) {
|
||||||
this.title = options.title
|
this.title = options.title
|
||||||
|
this.headerComponent = markRaw(options.headerComponent)
|
||||||
this.component = markRaw(options.component)
|
this.component = markRaw(options.component)
|
||||||
this.props = options.props || {}
|
this.props = options.props || {}
|
||||||
this.isVisible = true
|
this.isVisible = true
|
||||||
|
|||||||
@@ -10,7 +10,13 @@
|
|||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { ComfySettingsDialog } from '@/scripts/ui/settings'
|
import { ComfySettingsDialog } from '@/scripts/ui/settings'
|
||||||
import { SettingParams } from '@/types/settingTypes'
|
import { SettingParams } from '@/types/settingTypes'
|
||||||
|
import { buildTree } from '@/utils/treeUtil'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import type { TreeNode } from 'primevue/treenode'
|
||||||
|
|
||||||
|
export interface SettingTreeNode extends TreeNode {
|
||||||
|
data?: SettingParams
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
settingValues: Record<string, any>
|
settingValues: Record<string, any>
|
||||||
@@ -22,6 +28,13 @@ export const useSettingStore = defineStore('setting', {
|
|||||||
settingValues: {},
|
settingValues: {},
|
||||||
settings: {}
|
settings: {}
|
||||||
}),
|
}),
|
||||||
|
getters: {
|
||||||
|
settingTree(): SettingTreeNode {
|
||||||
|
return buildTree(Object.values(this.settings), (setting: SettingParams) =>
|
||||||
|
setting.id.split('.')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
addSettings(settings: ComfySettingsDialog) {
|
addSettings(settings: ComfySettingsDialog) {
|
||||||
for (const id in settings.settingsLookup) {
|
for (const id in settings.settingsLookup) {
|
||||||
|
|||||||
49
src/utils/treeUtil.ts
Normal file
49
src/utils/treeUtil.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { TreeNode } from 'primevue/treenode'
|
||||||
|
|
||||||
|
export function buildTree<T>(
|
||||||
|
items: T[],
|
||||||
|
key: string | ((item: T) => string[])
|
||||||
|
): TreeNode {
|
||||||
|
const root: TreeNode = {
|
||||||
|
key: 'root',
|
||||||
|
label: 'root',
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const map: Record<string, TreeNode> = {
|
||||||
|
root: root
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const keys = typeof key === 'string' ? item[key] : key(item)
|
||||||
|
let parent = root
|
||||||
|
for (const k of keys) {
|
||||||
|
const id = parent.key + '/' + k
|
||||||
|
if (!map[id]) {
|
||||||
|
const node: TreeNode = {
|
||||||
|
key: id,
|
||||||
|
label: k,
|
||||||
|
leaf: false,
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
map[id] = node
|
||||||
|
parent.children.push(node)
|
||||||
|
}
|
||||||
|
parent = map[id]
|
||||||
|
}
|
||||||
|
parent.leaf = true
|
||||||
|
parent.data = item
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenTree<T>(tree: TreeNode): T[] {
|
||||||
|
const result: T[] = []
|
||||||
|
const stack: TreeNode[] = [tree]
|
||||||
|
while (stack.length) {
|
||||||
|
const node = stack.pop()!
|
||||||
|
if (node.leaf && node.data) result.push(node.data)
|
||||||
|
stack.push(...(node.children || []))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user