Implement color palette in Vue (#2047)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2024-12-25 21:41:48 -05:00
committed by GitHub
parent f1eee96ebc
commit db572a4085
29 changed files with 501 additions and 608 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -128,7 +128,7 @@ describe('TreeExplorerTreeNode', () => {
expect(handleRenameMock).toHaveBeenCalledOnce()
expect(addToastSpy).toHaveBeenCalledWith({
severity: 'error',
summary: 'g.error',
summary: 'Error',
detail: 'Rename failed',
life: 3000
})

View File

@@ -32,6 +32,7 @@
<template #header>
<CurrentUserMessage v-if="tabValue === 'Comfy'" />
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
</template>
<SettingsPanel :settingGroups="sortedGroups(category)" />
</PanelTemplate>
@@ -77,6 +78,7 @@ import PanelTemplate from './setting/PanelTemplate.vue'
import AboutPanel from './setting/AboutPanel.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
import { flattenTree } from '@/utils/treeUtil'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'

View File

@@ -0,0 +1,63 @@
<template>
<Message severity="info" icon="pi pi-palette" pt:text="w-full">
<div class="flex items-center justify-between">
<div>
{{ $t('settingsCategories.ColorPalette') }}
</div>
<div class="actions">
<Select
class="w-44"
v-model="activePaletteId"
:options="palettes"
optionLabel="name"
optionValue="id"
/>
<Button
icon="pi pi-download"
text
:title="$t('g.download')"
@click="colorPaletteService.exportColorPalette(activePaletteId)"
/>
<Button
icon="pi pi-upload"
text
:title="$t('g.import')"
@click="importCustomPalette"
/>
<Button
icon="pi pi-trash"
severity="danger"
text
:title="$t('g.delete')"
@click="colorPaletteService.deleteCustomColorPalette(activePaletteId)"
:disabled="!colorPaletteStore.isCustomPalette(activePaletteId)"
/>
</div>
</div>
</Message>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import Select from 'primevue/select'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { storeToRefs } from 'pinia'
import { watch } from 'vue'
const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService()
const { palettes, activePaletteId } = storeToRefs(colorPaletteStore)
const importCustomPalette = async () => {
const palette = await colorPaletteService.importColorPalette()
if (palette) {
colorPaletteService.loadColorPalette(palette.id)
}
}
watch(activePaletteId, () => {
colorPaletteService.loadColorPalette(activePaletteId.value)
})
</script>

View File

@@ -65,6 +65,8 @@ import { ChangeTracker } from '@/scripts/changeTracker'
import { api } from '@/scripts/api'
import { useCommandStore } from '@/stores/commandStore'
import { workflowService } from '@/services/workflowService'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useColorPaletteService } from '@/services/colorPaletteService'
const emit = defineEmits(['ready'])
const canvasRef = ref<HTMLCanvasElement | null>(null)
@@ -209,6 +211,13 @@ watchEffect(() => {
canvasStore.canvas.canvas.style.cursor = 'default'
})
const colorPaletteService = useColorPaletteService()
watchEffect(() => {
if (!canvasStore.canvas) return
colorPaletteService.loadColorPalette(settingStore.get('Comfy.ColorPalette'))
})
const workflowStore = useWorkflowStore()
const persistCurrentWorkflow = () => {
const workflow = JSON.stringify(comfyApp.serializeGraph())
@@ -315,6 +324,12 @@ onMounted(async () => {
comfyAppReady.value = true
// Load color palette
const colorPaletteStore = useColorPaletteStore()
colorPaletteStore.customPalettes = settingStore.get(
'Comfy.CustomColorPalettes'
)
// Start watching for locale change after the initial value is loaded.
watch(
() => settingStore.get('Comfy.Locale'),

View File

@@ -7,10 +7,6 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import {
defaultColorPalette,
getColorPalette
} from '@/extensions/core/colorPalette'
import { app } from '@/scripts/app'
import type { LGraphNode } from '@comfyorg/litegraph'
import { BadgePosition } from '@comfyorg/litegraph'
@@ -18,9 +14,11 @@ import { LGraphBadge } from '@comfyorg/litegraph'
import _ from 'lodash'
import { NodeBadgeMode } from '@/types/nodeSource'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import type { Palette } from '@/types/colorPaletteTypes'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const settingStore = useSettingStore()
const colorPaletteStore = useColorPaletteStore()
const nodeSourceBadgeMode = computed(
() => settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode
)
@@ -36,10 +34,6 @@ watch([nodeSourceBadgeMode, nodeIdBadgeMode, nodeLifeCycleBadgeMode], () => {
app.graph?.setDirtyCanvas(true, true)
})
const colorPalette = computed<Palette | undefined>(() =>
getColorPalette(settingStore.get('Comfy.ColorPalette'))
)
const nodeDefStore = useNodeDefStore()
function badgeTextVisible(
nodeDef: ComfyNodeDefImpl | null,
@@ -79,11 +73,11 @@ onMounted(() => {
}
),
fgColor:
colorPalette.value?.colors?.litegraph_base?.BADGE_FG_COLOR ||
defaultColorPalette.colors.litegraph_base.BADGE_FG_COLOR,
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_FG_COLOR,
bgColor:
colorPalette.value?.colors?.litegraph_base?.BADGE_BG_COLOR ||
defaultColorPalette.colors.litegraph_base.BADGE_BG_COLOR
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_BG_COLOR
})
})

View File

@@ -81,12 +81,10 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
<script setup lang="ts">
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import {
getColorPalette,
defaultColorPalette
} from '@/extensions/core/colorPalette'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import _ from 'lodash'
import { useWidgetStore } from '@/stores/widgetStore'
import { computed } from 'vue'
const props = defineProps({
nodeDef: {
@@ -95,12 +93,10 @@ const props = defineProps({
}
})
// Node preview currently is recreated every time something is hovered.
// So not reactive to the color palette changes after setup is fine.
// If later we want NodePreview to be shown more persistently, then we should
// make the getColorPalette() call reactive.
const colors = getColorPalette()?.colors?.litegraph_base
const litegraphColors = colors ?? defaultColorPalette.colors.litegraph_base
const colorPaletteStore = useColorPaletteStore()
const litegraphColors = computed(
() => colorPaletteStore.completedActivePalette.colors.litegraph_base
)
const widgetStore = useWidgetStore()

View File

@@ -14,3 +14,5 @@ export const CORE_COLOR_PALETTES: ColorPalettes = {
nord,
github
} as const
export const DEFAULT_COLOR_PALETTE = dark

View File

@@ -1,3 +1,4 @@
import type { ColorPalettes } from '@/types/colorPaletteTypes'
import type { Keybinding } from '@/types/keyBindingTypes'
import { NodeBadgeMode } from '@/types/nodeSource'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
@@ -663,5 +664,19 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'boolean',
defaultValue: true,
versionAdded: '1.5.6'
},
{
id: 'Comfy.ColorPalette',
name: 'The active color palette id',
type: 'hidden',
defaultValue: 'dark',
versionModified: '1.6.7'
},
{
id: 'Comfy.CustomColorPalettes',
name: 'Custom color palettes',
type: 'hidden',
defaultValue: {} as ColorPalettes,
versionModified: '1.6.7'
}
]

View File

@@ -1,483 +0,0 @@
// @ts-strict-ignore
import { useToastStore } from '@/stores/toastStore'
import { app } from '../../scripts/app'
import { $el } from '../../scripts/ui'
import type { ColorPalettes, Palette } from '@/types/colorPaletteTypes'
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
import { CORE_COLOR_PALETTES } from '@/constants/coreColorPalettes'
// Manage color palettes
const colorPalettes = CORE_COLOR_PALETTES
const id = 'Comfy.ColorPalette'
const idCustomColorPalettes = 'Comfy.CustomColorPalettes'
const defaultColorPaletteId = 'dark'
const els: { select: HTMLSelectElement | null } = {
select: null
}
const getCustomColorPalettes = (): ColorPalettes => {
return app.ui.settings.getSettingValue(idCustomColorPalettes, {})
}
const setCustomColorPalettes = (customColorPalettes: ColorPalettes) => {
return app.ui.settings.setSettingValue(
idCustomColorPalettes,
customColorPalettes
)
}
export const defaultColorPalette = colorPalettes[defaultColorPaletteId]
export const getColorPalette = (
colorPaletteId?: string
): Palette | undefined => {
if (!colorPaletteId) {
colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId)
}
if (colorPaletteId.startsWith('custom_')) {
colorPaletteId = colorPaletteId.substr(7)
let customColorPalettes = getCustomColorPalettes()
if (customColorPalettes[colorPaletteId]) {
return customColorPalettes[colorPaletteId]
}
}
return colorPalettes[colorPaletteId]
}
const setColorPalette = (colorPaletteId) => {
app.ui.settings.setSettingValue(id, colorPaletteId)
}
// const ctxMenu = LiteGraph.ContextMenu;
app.registerExtension({
name: id,
init() {
/**
* Changes the background color of the canvas.
*
* @method updateBackground
* @param {image} String
* @param {clearBackgroundColor} String
*/
// @ts-expect-error
LGraphCanvas.prototype.updateBackground = function (
image,
clearBackgroundColor
) {
this._bg_img = new Image()
this._bg_img.name = image
this._bg_img.src = image
this._bg_img.onload = () => {
this.draw(true, true)
}
this.background_image = image
this.clear_background = true
this.clear_background_color = clearBackgroundColor
this._pattern = null
}
},
addCustomNodeDefs(node_defs) {
const sortObjectKeys = (unordered) => {
return Object.keys(unordered)
.sort()
.reduce((obj, key) => {
obj[key] = unordered[key]
return obj
}, {})
}
function getSlotTypes() {
var types = []
const defs = node_defs
for (const nodeId in defs) {
const nodeData = defs[nodeId]
var inputs = nodeData['input']['required']
if (nodeData['input']['optional'] !== undefined) {
inputs = Object.assign(
{},
nodeData['input']['required'],
nodeData['input']['optional']
)
}
for (const inputName in inputs) {
const inputData = inputs[inputName]
const type = inputData[0]
if (!Array.isArray(type)) {
types.push(type)
}
}
for (const o in nodeData['output']) {
const output = nodeData['output'][o]
types.push(output)
}
}
return types
}
function completeColorPalette(colorPalette) {
var types = getSlotTypes()
for (const type of types) {
if (!colorPalette.colors.node_slot[type]) {
colorPalette.colors.node_slot[type] = ''
}
}
colorPalette.colors.node_slot = sortObjectKeys(
colorPalette.colors.node_slot
)
return colorPalette
}
const getColorPaletteTemplate = async () => {
const colorPalette: Palette = {
id: 'my_color_palette_unique_id',
name: 'My Color Palette',
colors: {
node_slot: {},
litegraph_base: {},
comfy_base: {}
}
}
// Copy over missing keys from default color palette
const defaultColorPalette = colorPalettes[defaultColorPaletteId]
for (const key in defaultColorPalette.colors.litegraph_base) {
colorPalette.colors.litegraph_base[key] ||= ''
}
for (const key in defaultColorPalette.colors.comfy_base) {
colorPalette.colors.comfy_base[key] ||= ''
}
return completeColorPalette(colorPalette)
}
const addCustomColorPalette = async (colorPalette) => {
if (typeof colorPalette !== 'object') {
useToastStore().addAlert('Invalid color palette.')
return
}
if (!colorPalette.id) {
useToastStore().addAlert('Color palette missing id.')
return
}
if (!colorPalette.name) {
useToastStore().addAlert('Color palette missing name.')
return
}
if (!colorPalette.colors) {
useToastStore().addAlert('Color palette missing colors.')
return
}
if (
colorPalette.colors.node_slot &&
typeof colorPalette.colors.node_slot !== 'object'
) {
useToastStore().addAlert('Invalid color palette colors.node_slot.')
return
}
const customColorPalettes = getCustomColorPalettes()
customColorPalettes[colorPalette.id] = colorPalette
setCustomColorPalettes(customColorPalettes)
for (const option of els.select.childNodes) {
if (
(option as HTMLOptionElement).value ===
'custom_' + colorPalette.id
) {
els.select.removeChild(option)
}
}
els.select.append(
$el('option', {
textContent: colorPalette.name + ' (custom)',
value: 'custom_' + colorPalette.id,
selected: true
})
)
setColorPalette('custom_' + colorPalette.id)
await loadColorPalette(colorPalette)
}
const deleteCustomColorPalette = async (colorPaletteId) => {
const customColorPalettes = getCustomColorPalettes()
delete customColorPalettes[colorPaletteId]
setCustomColorPalettes(customColorPalettes)
for (const opt of els.select.childNodes) {
const option = opt as HTMLOptionElement
if (option.value === defaultColorPaletteId) {
option.selected = true
}
if (option.value === 'custom_' + colorPaletteId) {
els.select.removeChild(option)
}
}
setColorPalette(defaultColorPaletteId)
await loadColorPalette(getColorPalette())
}
const loadColorPalette = async (colorPalette: Palette) => {
colorPalette = await completeColorPalette(colorPalette)
if (colorPalette.colors) {
// Sets the colors of node slots and links
if (colorPalette.colors.node_slot) {
Object.assign(
app.canvas.default_connection_color_byType,
colorPalette.colors.node_slot
)
Object.assign(
LGraphCanvas.link_type_colors,
colorPalette.colors.node_slot
)
}
// Sets the colors of the LiteGraph objects
if (colorPalette.colors.litegraph_base) {
// Everything updates correctly in the loop, except the Node Title and Link Color for some reason
app.canvas.node_title_color =
colorPalette.colors.litegraph_base.NODE_TITLE_COLOR
app.canvas.default_link_color =
colorPalette.colors.litegraph_base.LINK_COLOR
for (const key in colorPalette.colors.litegraph_base) {
if (
colorPalette.colors.litegraph_base.hasOwnProperty(key) &&
LiteGraph.hasOwnProperty(key)
) {
const value = colorPalette.colors.litegraph_base[key]
if (key === 'NODE_DEFAULT_SHAPE' && typeof value === 'string') {
console.warn(
`litegraph_base.NODE_DEFAULT_SHAPE only accepts [${[
LiteGraph.BOX_SHAPE,
LiteGraph.ROUND_SHAPE,
LiteGraph.CARD_SHAPE
].join(', ')}] but got ${value}`
)
LiteGraph.NODE_DEFAULT_SHAPE = LiteGraph.ROUND_SHAPE
} else {
LiteGraph[key] = value
}
}
}
}
// Sets the color of ComfyUI elements
if (colorPalette.colors.comfy_base) {
const rootStyle = document.documentElement.style
for (const key in colorPalette.colors.comfy_base) {
rootStyle.setProperty(
'--' + key,
colorPalette.colors.comfy_base[key]
)
}
}
// Sets special case colors
if (colorPalette.colors.litegraph_base.NODE_BYPASS_BGCOLOR) {
app.bypassBgColor =
colorPalette.colors.litegraph_base.NODE_BYPASS_BGCOLOR
}
app.canvas.setDirty(true, true)
}
}
const fileInput = $el('input', {
type: 'file',
accept: '.json',
style: { display: 'none' },
parent: document.body,
onchange: () => {
const file = fileInput.files[0]
if (file.type === 'application/json' || file.name.endsWith('.json')) {
const reader = new FileReader()
reader.onload = async () => {
await addCustomColorPalette(JSON.parse(reader.result as string))
}
reader.readAsText(file)
}
}
}) as HTMLInputElement
app.ui.settings.addSetting({
id,
category: ['Appearance', 'ColorPalette'],
name: 'Color Palette',
type: (name, setter, value) => {
const options = [
...Object.values(colorPalettes).map((c) =>
$el('option', {
textContent: c.name,
value: c.id,
selected: c.id === value
})
),
...Object.values(getCustomColorPalettes()).map((c) =>
$el('option', {
textContent: `${c.name} (custom)`,
value: `custom_${c.id}`,
selected: `custom_${c.id}` === value
})
)
]
els.select = $el(
'select',
{
style: {
marginBottom: '0.15rem',
width: '100%'
},
onchange: (e) => {
setter(e.target.value)
}
},
options
) as HTMLSelectElement
return $el('tr', [
$el('td', [
els.select,
$el(
'div',
{
style: {
display: 'grid',
gap: '4px',
gridAutoFlow: 'column'
}
},
[
$el('input', {
type: 'button',
value: 'Export',
onclick: async () => {
const colorPaletteId = app.ui.settings.getSettingValue(
id,
defaultColorPaletteId
)
const colorPalette = await completeColorPalette(
getColorPalette(colorPaletteId)
)
const json = JSON.stringify(colorPalette, null, 2) // convert the data to a JSON string
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
download: colorPaletteId + '.json',
style: { display: 'none' },
parent: document.body
})
a.click()
setTimeout(function () {
a.remove()
window.URL.revokeObjectURL(url)
}, 0)
}
}),
$el('input', {
type: 'button',
value: 'Import',
onclick: () => {
fileInput.click()
}
}),
$el('input', {
type: 'button',
value: 'Template',
onclick: async () => {
const colorPalette = await getColorPaletteTemplate()
const json = JSON.stringify(colorPalette, null, 2) // convert the data to a JSON string
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
download: 'color_palette.json',
style: { display: 'none' },
parent: document.body
})
a.click()
setTimeout(function () {
a.remove()
window.URL.revokeObjectURL(url)
}, 0)
}
}),
$el('input', {
type: 'button',
value: 'Delete',
onclick: async () => {
let colorPaletteId = app.ui.settings.getSettingValue(
id,
defaultColorPaletteId
)
if (colorPalettes[colorPaletteId]) {
useToastStore().addAlert(
'You cannot delete a built-in color palette.'
)
return
}
if (colorPaletteId.startsWith('custom_')) {
colorPaletteId = colorPaletteId.substr(7)
}
await deleteCustomColorPalette(colorPaletteId)
}
})
]
)
])
])
},
defaultValue: defaultColorPaletteId,
async onChange(value) {
if (!value) {
return
}
let palette = colorPalettes[value]
if (palette) {
await loadColorPalette(palette)
} else if (value.startsWith('custom_')) {
value = value.substr(7)
let customColorPalettes = getCustomColorPalettes()
if (customColorPalettes[value]) {
palette = customColorPalettes[value]
await loadColorPalette(customColorPalettes[value])
}
}
let { BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR } =
palette.colors.litegraph_base
if (
BACKGROUND_IMAGE === undefined ||
CLEAR_BACKGROUND_COLOR === undefined
) {
const base = colorPalettes['dark'].colors.litegraph_base
BACKGROUND_IMAGE = base.BACKGROUND_IMAGE
CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR
}
// @ts-expect-error
// litegraph.extensions.js
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
}
})
}
})

View File

@@ -1,5 +1,4 @@
import './clipspace'
import './colorPalette'
import './contextMenuFilter'
import './dynamicPrompts'
import './editAttention'

View File

@@ -1,9 +1,8 @@
import { t } from '@/i18n'
import { useToastStore } from '@/stores/toastStore'
import { useI18n } from 'vue-i18n'
export function useErrorHandling() {
const toast = useToastStore()
const { t } = useI18n()
const toastErrorHandler = (error: any) => {
toast.add({
@@ -15,12 +14,12 @@ export function useErrorHandling() {
}
const wrapWithErrorHandling =
(
action: (...args: any[]) => any,
<TArgs extends any[], TReturn>(
action: (...args: TArgs) => TReturn,
errorHandler?: (error: any) => void,
finallyHandler?: () => void
) =>
(...args: any[]) => {
(...args: TArgs): TReturn | undefined => {
try {
return action(...args)
} catch (e) {
@@ -31,12 +30,12 @@ export function useErrorHandling() {
}
const wrapWithErrorHandlingAsync =
(
action: ((...args: any[]) => Promise<any>) | ((...args: any[]) => any),
<TArgs extends any[], TReturn>(
action: (...args: TArgs) => Promise<TReturn> | TReturn,
errorHandler?: (error: any) => void,
finallyHandler?: () => void
) =>
async (...args: any[]) => {
async (...args: TArgs): Promise<TReturn | undefined> => {
try {
return await action(...args)
} catch (e) {

View File

@@ -6,6 +6,7 @@
"comingSoon": "Coming Soon",
"firstTimeUIMessage": "This is the first time you use the new UI. Choose \"Menu > Use New Menu > Disabled\" to restore the old UI.",
"download": "Download",
"import": "Import",
"loadAllFolders": "Load All Folders",
"refresh": "Refresh",
"terminal": "Terminal",
@@ -427,7 +428,8 @@
"Window": "Window",
"Server-Config": "Server-Config",
"About": "About",
"EditTokenWeight": "Edit Token Weight"
"EditTokenWeight": "Edit Token Weight",
"CustomColorPalettes": "Custom Color Palettes"
},
"serverConfigItems": {
"listen": {

View File

@@ -5,9 +5,6 @@
"Comfy-Desktop_SendStatistics": {
"name": "Send anonymous crash reports"
},
"Comfy_ColorPalette": {
"name": "Color Palette"
},
"Comfy_ConfirmClear": {
"name": "Require confirmation when clearing workflow"
},

View File

@@ -91,6 +91,7 @@
"goToNode": "ノードに移動",
"icon": "アイコン",
"imageFailedToLoad": "画像の読み込みに失敗しました",
"import": "インポート",
"insert": "挿入",
"install": "インストール",
"keybinding": "キーバインディング",
@@ -541,6 +542,7 @@
"ColorPalette": "カラーパレット",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfyデスクトップ",
"CustomColorPalettes": "カスタムカラーパレット",
"DevMode": "開発モード",
"EditTokenWeight": "トークンの重みを編集",
"Extension": "拡張",

View File

@@ -5,9 +5,6 @@
"Comfy-Desktop_SendStatistics": {
"name": "匿名のクラッシュレポートを送信する"
},
"Comfy_ColorPalette": {
"name": "カラーパレット"
},
"Comfy_ConfirmClear": {
"name": "ワークフローをクリアする際に確認を要求する"
},

View File

@@ -91,6 +91,7 @@
"goToNode": "노드로 이동",
"icon": "아이콘",
"imageFailedToLoad": "이미지를 로드하지 못했습니다.",
"import": "가져오기",
"insert": "삽입",
"install": "설치",
"keybinding": "키 바인딩",
@@ -541,6 +542,7 @@
"ColorPalette": "색상 팔레트",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy-Desktop",
"CustomColorPalettes": "사용자 정의 색상 팔레트",
"DevMode": "개발자 모드",
"EditTokenWeight": "토큰 가중치 편집",
"Extension": "확장",

View File

@@ -5,9 +5,6 @@
"Comfy-Desktop_SendStatistics": {
"name": "익명으로 충돌 보고서 전송"
},
"Comfy_ColorPalette": {
"name": "색상 팔레트"
},
"Comfy_ConfirmClear": {
"name": "워크플로 비우기 시 확인 요구"
},

View File

@@ -91,6 +91,7 @@
"goToNode": "Перейти к узлу",
"icon": "Иконка",
"imageFailedToLoad": "Не удалось загрузить изображение",
"import": "Импорт",
"insert": "Вставить",
"install": "Установить",
"keybinding": "Привязка клавиш",
@@ -541,6 +542,7 @@
"ColorPalette": "Цветовая палитра",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy рабочий стол",
"CustomColorPalettes": "Пользовательские цветовые палитры",
"DevMode": "Режим разработчика",
"EditTokenWeight": "Редактировать вес токена",
"Extension": "Расширение",

View File

@@ -5,9 +5,6 @@
"Comfy-Desktop_SendStatistics": {
"name": "Отправлять анонимные отчеты о сбоях"
},
"Comfy_ColorPalette": {
"name": "Цветовая палитра"
},
"Comfy_ConfirmClear": {
"name": "Требовать подтверждение при очистке рабочего процесса"
},

View File

@@ -91,6 +91,7 @@
"goToNode": "转到节点",
"icon": "图标",
"imageFailedToLoad": "图像加载失败",
"import": "导入",
"insert": "插入",
"install": "安装",
"keybinding": "快捷键",
@@ -541,6 +542,7 @@
"ColorPalette": "调色板",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy桌面版",
"CustomColorPalettes": "自定义颜色调色板",
"DevMode": "开发模式",
"EditTokenWeight": "编辑令牌权重",
"Extension": "扩展",

View File

@@ -5,9 +5,6 @@
"Comfy-Desktop_SendStatistics": {
"name": "发送匿名崩溃报告"
},
"Comfy_ColorPalette": {
"name": "调色板"
},
"Comfy_ConfirmClear": {
"name": "清除工作流时需要确认"
},

View File

@@ -110,11 +110,7 @@ export async function addStylesheet(
})
}
/**
* @param { string } filename
* @param { Blob } blob
*/
export function downloadBlob(filename, blob) {
export function downloadBlob(filename: string, blob: Blob) {
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
@@ -129,6 +125,20 @@ export function downloadBlob(filename, blob) {
}, 0)
}
export function uploadFile(accept: string) {
return new Promise<File>((resolve, reject) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = accept
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return reject(new Error('No file selected'))
resolve(file)
}
input.click()
})
}
export function prop<T>(
target: object,
name: string,

View File

@@ -0,0 +1,186 @@
import { useSettingStore } from '@/stores/settingStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useErrorHandling } from '@/hooks/errorHooks'
import { Colors, paletteSchema, type Palette } from '@/types/colorPaletteTypes'
import { fromZodError } from 'zod-validation-error'
import { LGraphCanvas } from '@comfyorg/litegraph'
import { LiteGraph } from '@comfyorg/litegraph'
import { app } from '@/scripts/app'
import { downloadBlob, uploadFile } from '@/scripts/utils'
import { toRaw } from 'vue'
export const useColorPaletteService = () => {
const colorPaletteStore = useColorPaletteStore()
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const { wrapWithErrorHandling, wrapWithErrorHandlingAsync } =
useErrorHandling()
/**
* Validates the palette against the zod schema.
*
* @param data - The palette to validate.
* @returns The validated palette.
*/
const validateColorPalette = (data: unknown): Palette => {
const result = paletteSchema.safeParse(data)
if (result.success) return result.data
const error = fromZodError(result.error)
throw new Error(`Invalid color palette against zod schema:\n${error}`)
}
const persistCustomColorPalettes = () => {
settingStore.set(
'Comfy.CustomColorPalettes',
colorPaletteStore.customPalettes
)
}
/**
* Deletes a custom color palette.
*
* @param colorPaletteId - The ID of the color palette to delete.
*/
const deleteCustomColorPalette = (colorPaletteId: string) => {
colorPaletteStore.deleteCustomPalette(colorPaletteId)
persistCustomColorPalettes()
}
/**
* Adds a custom color palette.
*
* @param colorPalette - The palette to add.
*/
const addCustomColorPalette = (colorPalette: Palette) => {
validateColorPalette(colorPalette)
colorPaletteStore.addCustomPalette(colorPalette)
persistCustomColorPalettes()
}
/**
* Sets the colors of node slots and links.
*
* @param linkColorPalette - The palette to set.
*/
const loadLinkColorPalette = (linkColorPalette: Colors['node_slot']) => {
const types = Object.fromEntries(
Array.from(nodeDefStore.nodeDataTypes).map((type) => [type, ''])
)
Object.assign(
app.canvas.default_connection_color_byType,
types,
linkColorPalette
)
Object.assign(LGraphCanvas.link_type_colors, types, linkColorPalette)
}
/**
* Loads the LiteGraph color palette.
*
* @param liteGraphColorPalette - The palette to set.
*/
const loadLiteGraphColorPalette = (palette: Colors['litegraph_base']) => {
// Sets special case colors
app.bypassBgColor = palette.NODE_BYPASS_BGCOLOR
// Sets the colors of the LiteGraph objects
app.canvas.node_title_color = palette.NODE_TITLE_COLOR
app.canvas.default_link_color = palette.LINK_COLOR
app.canvas.background_image = palette.BACKGROUND_IMAGE
app.canvas.clear_background_color = palette.CLEAR_BACKGROUND_COLOR
app.canvas._pattern = undefined
for (const [key, value] of Object.entries(palette)) {
if (Object.prototype.hasOwnProperty.call(LiteGraph, key)) {
if (key === 'NODE_DEFAULT_SHAPE' && typeof value === 'string') {
console.warn(
`litegraph_base.NODE_DEFAULT_SHAPE only accepts [${[
LiteGraph.BOX_SHAPE,
LiteGraph.ROUND_SHAPE,
LiteGraph.CARD_SHAPE
].join(', ')}] but got ${value}`
)
LiteGraph.NODE_DEFAULT_SHAPE = LiteGraph.ROUND_SHAPE
} else {
;(LiteGraph as any)[key] = value
}
}
}
}
/**
* Loads the Comfy color palette.
*
* @param comfyColorPalette - The palette to set.
*/
const loadComfyColorPalette = (comfyColorPalette: Colors['comfy_base']) => {
if (comfyColorPalette) {
const rootStyle = document.documentElement.style
for (const [key, value] of Object.entries(comfyColorPalette)) {
rootStyle.setProperty('--' + key, value)
}
}
}
/**
* Loads the color palette.
*
* @param colorPaletteId - The ID of the color palette to load.
*/
const loadColorPalette = async (colorPaletteId: string) => {
const colorPalette = colorPaletteStore.palettesLookup[colorPaletteId]
if (!colorPalette) {
throw new Error(`Color palette ${colorPaletteId} not found`)
}
const completedPalette = colorPaletteStore.completePalette(colorPalette)
loadLinkColorPalette(completedPalette.colors.node_slot)
loadLiteGraphColorPalette(completedPalette.colors.litegraph_base)
loadComfyColorPalette(completedPalette.colors.comfy_base)
app.canvas.setDirty(true, true)
colorPaletteStore.activePaletteId = colorPaletteId
settingStore.set('Comfy.ColorPalette', colorPaletteId)
}
/**
* Exports a color palette.
*
* @param colorPaletteId - The ID of the color palette to export.
*/
const exportColorPalette = (colorPaletteId: string) => {
const colorPalette = colorPaletteStore.palettesLookup[colorPaletteId]
if (!colorPalette) {
throw new Error(`Color palette ${colorPaletteId} not found`)
}
downloadBlob(
colorPalette.id + '.json',
new Blob([JSON.stringify(toRaw(colorPalette), null, 2)], {
type: 'application/json'
})
)
}
/**
* Imports a color palette.
*
* @returns The imported palette.
*/
const importColorPalette = async () => {
const file = await uploadFile('application/json')
const text = await file.text()
const palette = JSON.parse(text)
addCustomColorPalette(palette)
return palette
}
return {
addCustomColorPalette: wrapWithErrorHandling(addCustomColorPalette),
deleteCustomColorPalette: wrapWithErrorHandling(deleteCustomColorPalette),
loadColorPalette: wrapWithErrorHandling(loadColorPalette),
exportColorPalette: wrapWithErrorHandling(exportColorPalette),
importColorPalette: wrapWithErrorHandlingAsync(importColorPalette)
}
}

View File

@@ -11,13 +11,8 @@ import { useWidgetStore } from './widgetStore'
/**
* These extensions are always active, even if they are disabled in the setting.
* TODO(https://github.com/Comfy-Org/ComfyUI_frontend/issues/1996):
* Migrate logic to out of extensions/core, as features provided
* by these extensions are now essential to core.
*/
export const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = [
'Comfy.ColorPalette'
]
export const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
export const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
// pysssss.Locking is replaced by pin/unpin in ComfyUI core.

View File

@@ -319,6 +319,18 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
const showExperimental = ref(false)
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
const nodeDataTypes = computed(() => {
const types = new Set<string>()
for (const nodeDef of nodeDefs.value) {
for (const input of nodeDef.inputs.all) {
types.add(input.type)
}
for (const output of nodeDef.outputs.all) {
types.add(output.type)
}
}
return types
})
const visibleNodeDefs = computed(() =>
nodeDefs.value.filter(
(nodeDef: ComfyNodeDefImpl) =>
@@ -365,6 +377,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
showExperimental,
nodeDefs,
nodeDataTypes,
visibleNodeDefs,
nodeSearchService,
nodeTree,

View File

@@ -0,0 +1,92 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type {
ColorPalettes,
CompletedPalette,
Palette
} from '@/types/colorPaletteTypes'
import {
CORE_COLOR_PALETTES,
DEFAULT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
export const useColorPaletteStore = defineStore('colorPalette', () => {
const customPalettes = ref<ColorPalettes>({})
const activePaletteId = ref<string>(DEFAULT_COLOR_PALETTE.id)
const palettesLookup = computed(() => ({
...CORE_COLOR_PALETTES,
...customPalettes.value
}))
const palettes = computed(() => Object.values(palettesLookup.value))
const completedActivePalette = computed(() =>
completePalette(palettesLookup.value[activePaletteId.value])
)
const addCustomPalette = (palette: Palette) => {
if (palette.id in palettesLookup.value) {
throw new Error(`Palette with id ${palette.id} already exists`)
}
customPalettes.value[palette.id] = palette
activePaletteId.value = palette.id
}
const deleteCustomPalette = (id: string) => {
if (!(id in customPalettes.value)) {
throw new Error(`Palette with id ${id} does not exist`)
}
delete customPalettes.value[id]
activePaletteId.value = CORE_COLOR_PALETTES.dark.id
}
const isCustomPalette = (id: string) => {
return id in customPalettes.value
}
/**
* Completes the palette with default values for missing colors.
*
* @param palette - The palette to complete.
* @returns The completed palette.
*/
const completePalette = (palette: Palette): CompletedPalette => {
return {
...palette,
colors: {
...palette.colors,
node_slot: {
...DEFAULT_COLOR_PALETTE.colors.node_slot,
...palette.colors.node_slot
},
litegraph_base: {
...DEFAULT_COLOR_PALETTE.colors.litegraph_base,
...palette.colors.litegraph_base
},
comfy_base: {
...DEFAULT_COLOR_PALETTE.colors.comfy_base,
...palette.colors.comfy_base
}
}
}
}
return {
// State
customPalettes,
activePaletteId,
// Getters
palettesLookup,
palettes,
completedActivePalette,
// Actions
isCustomPalette,
addCustomPalette,
deleteCustomPalette,
completePalette
}
})

View File

@@ -2,65 +2,54 @@ import { LiteGraph } from '@comfyorg/litegraph'
import { z } from 'zod'
const nodeSlotSchema = z.object({
BOOLEAN: z.string().optional(),
CLIP: z.string().optional(),
CLIP_VISION: z.string().optional(),
CLIP_VISION_OUTPUT: z.string().optional(),
CONDITIONING: z.string().optional(),
CONTROL_NET: z.string().optional(),
CONTROL_NET_WEIGHTS: z.string().optional(),
FLOAT: z.string().optional(),
GLIGEN: z.string().optional(),
IMAGE: z.string().optional(),
IMAGEUPLOAD: z.string().optional(),
INT: z.string().optional(),
LATENT: z.string().optional(),
LATENT_KEYFRAME: z.string().optional(),
MASK: z.string().optional(),
MODEL: z.string().optional(),
SAMPLER: z.string().optional(),
SIGMAS: z.string().optional(),
STRING: z.string().optional(),
STYLE_MODEL: z.string().optional(),
T2I_ADAPTER_WEIGHTS: z.string().optional(),
TAESD: z.string().optional(),
TIMESTEP_KEYFRAME: z.string().optional(),
UPSCALE_MODEL: z.string().optional(),
VAE: z.string().optional()
CLIP: z.string(),
CLIP_VISION: z.string(),
CLIP_VISION_OUTPUT: z.string(),
CONDITIONING: z.string(),
CONTROL_NET: z.string(),
IMAGE: z.string(),
LATENT: z.string(),
MASK: z.string(),
MODEL: z.string(),
STYLE_MODEL: z.string(),
VAE: z.string(),
NOISE: z.string(),
GUIDER: z.string(),
SAMPLER: z.string(),
SIGMAS: z.string(),
TAESD: z.string()
})
const litegraphBaseSchema = z.object({
BACKGROUND_IMAGE: z.string().optional(),
CLEAR_BACKGROUND_COLOR: z.string().optional(),
NODE_TITLE_COLOR: z.string().optional(),
NODE_SELECTED_TITLE_COLOR: z.string().optional(),
NODE_TEXT_SIZE: z.number().optional(),
NODE_TEXT_COLOR: z.string().optional(),
NODE_SUBTEXT_SIZE: z.number().optional(),
NODE_DEFAULT_COLOR: z.string().optional(),
NODE_DEFAULT_BGCOLOR: z.string().optional(),
NODE_DEFAULT_BOXCOLOR: z.string().optional(),
NODE_DEFAULT_SHAPE: z
.union([
z.literal(LiteGraph.BOX_SHAPE),
z.literal(LiteGraph.ROUND_SHAPE),
z.literal(LiteGraph.CARD_SHAPE)
])
.optional(),
NODE_BOX_OUTLINE_COLOR: z.string().optional(),
NODE_BYPASS_BGCOLOR: z.string().optional(),
NODE_ERROR_COLOUR: z.string().optional(),
DEFAULT_SHADOW_COLOR: z.string().optional(),
DEFAULT_GROUP_FONT: z.number().optional(),
WIDGET_BGCOLOR: z.string().optional(),
WIDGET_OUTLINE_COLOR: z.string().optional(),
WIDGET_TEXT_COLOR: z.string().optional(),
WIDGET_SECONDARY_TEXT_COLOR: z.string().optional(),
LINK_COLOR: z.string().optional(),
EVENT_LINK_COLOR: z.string().optional(),
CONNECTING_LINK_COLOR: z.string().optional(),
BADGE_FG_COLOR: z.string().optional(),
BADGE_BG_COLOR: z.string().optional()
BACKGROUND_IMAGE: z.string(),
CLEAR_BACKGROUND_COLOR: z.string(),
NODE_TITLE_COLOR: z.string(),
NODE_SELECTED_TITLE_COLOR: z.string(),
NODE_TEXT_SIZE: z.number(),
NODE_TEXT_COLOR: z.string(),
NODE_SUBTEXT_SIZE: z.number(),
NODE_DEFAULT_COLOR: z.string(),
NODE_DEFAULT_BGCOLOR: z.string(),
NODE_DEFAULT_BOXCOLOR: z.string(),
NODE_DEFAULT_SHAPE: z.union([
z.literal(LiteGraph.BOX_SHAPE),
z.literal(LiteGraph.ROUND_SHAPE),
z.literal(LiteGraph.CARD_SHAPE)
]),
NODE_BOX_OUTLINE_COLOR: z.string(),
NODE_BYPASS_BGCOLOR: z.string(),
NODE_ERROR_COLOUR: z.string(),
DEFAULT_SHADOW_COLOR: z.string(),
DEFAULT_GROUP_FONT: z.number(),
WIDGET_BGCOLOR: z.string(),
WIDGET_OUTLINE_COLOR: z.string(),
WIDGET_TEXT_COLOR: z.string(),
WIDGET_SECONDARY_TEXT_COLOR: z.string(),
LINK_COLOR: z.string(),
EVENT_LINK_COLOR: z.string(),
CONNECTING_LINK_COLOR: z.string(),
BADGE_FG_COLOR: z.string(),
BADGE_BG_COLOR: z.string()
})
const comfyBaseSchema = z.object({
@@ -68,7 +57,7 @@ const comfyBaseSchema = z.object({
['bg-color']: z.string(),
['bg-img']: z.string().optional(),
['comfy-menu-bg']: z.string(),
['comfy-menu-secondary-bg']: z.string().optional(),
['comfy-menu-secondary-bg']: z.string(),
['comfy-input-bg']: z.string(),
['input-text']: z.string(),
['descrip-text']: z.string(),
@@ -84,15 +73,25 @@ const comfyBaseSchema = z.object({
['bar-shadow']: z.string()
})
const colorsSchema = z
.object({
node_slot: nodeSlotSchema,
litegraph_base: litegraphBaseSchema,
comfy_base: comfyBaseSchema
})
.passthrough()
const colorsSchema = z.object({
node_slot: nodeSlotSchema,
litegraph_base: litegraphBaseSchema,
comfy_base: comfyBaseSchema
})
const paletteSchema = z.object({
const partialColorsSchema = z.object({
node_slot: nodeSlotSchema.partial(),
litegraph_base: litegraphBaseSchema.partial(),
comfy_base: comfyBaseSchema.partial()
})
export const paletteSchema = z.object({
id: z.string(),
name: z.string(),
colors: partialColorsSchema
})
export const completedPaletteSchema = z.object({
id: z.string(),
name: z.string(),
colors: colorsSchema
@@ -102,4 +101,5 @@ export const colorPalettesSchema = z.record(paletteSchema)
export type Colors = z.infer<typeof colorsSchema>
export type Palette = z.infer<typeof paletteSchema>
export type CompletedPalette = z.infer<typeof completedPaletteSchema>
export type ColorPalettes = z.infer<typeof colorPalettesSchema>